ESM vs CommonJS
1. In short: the main idea
| System | Core concept |
|---|---|
| CommonJS (CJS) | Modules are loaded at runtime, synchronously, line by line. |
| ES Modules (ESM) | Modules are analyzed at compile time (parse-time), loaded asynchronously, with strict dependencies and "live" imports. |
2. Philosophy of the systems
CommonJS
Created for Node.js (in 2009) to run JS on the server.
- Everything is synchronous (since files are on the local disk);
- Each file is an isolated module;
- Code executes top to bottom on the first
require(); - Imports are a copy of values, not "live references".
Example:
// math.js
module.exports = { x: 1 };
// app.js
const math = require('./math');
console.log(math.x); // 1
math.x = 2; // changes only the local copyES Modules
Created for the JavaScript language (ES6, 2015), so that one standard would work in both browsers and Node.js.
- Loading is asynchronous, declarative;
- Imports are static - known before the code runs;
- Optimization is possible, including tree-shaking;
- Imports are live bindings, that is, "live" references rather than copies.
Example:
// counter.js
export let count = 0;
export function inc() { count++; }
// app.js
import { count, inc } from './counter.js';
inc();
console.log(count); // 1 (the value updated)3. Conceptual differences
| Concept | CommonJS | ES Modules |
|---|---|---|
| Load time | At runtime | Before execution (parse-time) |
| Imports | Dynamic (require()) | Static (import) |
| Binding type | Values are copied | Live references (live bindings) |
| Execution order | Top to bottom | Determined by dependencies |
| Asynchrony | Synchronous | Asynchronous |
| Optimization | Not possible (runtime) | Tree-shaking possible |
| Context | this = module.exports | this = undefined |
| Caching | After the first call | Yes, but with "live" values |
| Ecosystem | Node.js, older packages | Modern JS, browsers, Node 13+ |
| Architecture | Imperative | Declarative |
| Compatibility | Server only | Universal (server + client) |
4. The difference in loading and compilation timing
CommonJS:
-
The line
require()executes -> the file is read -> the code runs. -
So the path can be a variable:
javascriptconst lib = require(condition ? './a' : './b');
It works because this is an ordinary function.
ES Modules:
-
Imports must be at the top of the file and cannot be dynamic:
javascriptimport x from './a'; // valid if (cond) import x from './b'; // error -
Node.js and the browser know in advance which dependencies are needed,
and can load them in parallel before the code runs.
5. The difference in module structure
Node.js "wraps" a CommonJS module in an internal function:
(function (exports, require, module, __filename, __dirname) {
// your code
});So there is:
requiremodule.exports__dirname__filename
This is absent in ESM:
- instead of
__dirname->import.meta.url requireis unavailable
6. Caching behavior
In CommonJS, a module executes once and is cached:
const a = require('./a');
const b = require('./a');
console.log(a === b); // trueIn ESM there is also a cache, but imports remain live (if the variables are exported as let/const):
import { counter } from './a.js';If the module updates the value, all imports see the update.
7. Tree-shaking and optimization
Tree-shaking is the removal of unused code (in Webpack, Rollup, Vite, etc.).
- CJS: not possible, because imports are dynamic;
- ESM: possible, because dependencies are static.
Example:
import { used } from './utils.js'; // the bundler knows only used is needed8. Compatibility
| Scenario | CommonJS | ES Modules |
|---|---|---|
| Using older packages | Yes | Sometimes needs createRequire() |
| SSR / server | Excellent | Excellent (Node 13+) |
| Browsers | No | Yes |
| Tree-shaking | No | Yes |
| Top-level await | No | Yes |
9. "Live" vs "dead" imports (example)
CommonJS:
// counter.js
let count = 0;
module.exports = { count, inc() { count++; } };
// app.js
const c = require('./counter');
c.inc();
console.log(c.count); // 0 (a copy)ES Modules:
// counter.mjs
export let count = 0;
export function inc() { count++; }
// app.mjs
import { count, inc } from './counter.mjs';
inc();
console.log(count); // 1 (a live reference)10. Conceptually: the key differences
| Parameter | CommonJS | ESM |
|---|---|---|
| Basis | Node.js (server) | ECMAScript standard |
| Execution model | Executes immediately (runtime) | Analyzes dependencies first |
| Imports | Allowed anywhere | Top level only |
| Access to the environment | require, module, exports, __dirname | import.meta.url, import/export |
| Compatibility | Many older libraries | Modern bundlers and browsers |
| Performance | Faster for local requires | More efficient in large projects |
In one sentence:
CommonJS is a dynamic, synchronous, Node.js-specific module system where imports execute at startup.
ES Modules is a static, asynchronous, standardized system where dependencies are known in advance and import creates live references.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.