ESM vs CommonJS
1. In short: the core idea
| System | Core concept |
|---|---|
| CommonJS (CJS) | Modules load at runtime, synchronously, line by line. |
| ES Modules (ESM) | Modules are analyzed at parse time, load asynchronously, with strict dependencies and "live" imports. |
2. The philosophy behind each
CommonJS
Built for Node.js (2009), to run JS on the server.
- Everything is synchronous (since files sit on the local disk);
- Every file is an isolated module;
- Code runs top to bottom on the first
require(); - Imports are copies of values, not "live bindings".
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
Built for the JavaScript language (ES6, 2015), so one standard could work in both browsers and Node.js.
- Loading is asynchronous, declarative;
- Imports are static, known before the code runs;
- They can be optimized, enabling tree-shaking;
- Imports are live bindings, not 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. The conceptual differences
| Concept | CommonJS | ES Modules |
|---|---|---|
| When it loads | At runtime | Before execution (parse time) |
| Imports | Dynamic (require()) | Static (import) |
| Binding type | Values are copied | Live bindings |
| Execution order | Top to bottom | Determined by dependencies |
| Asynchrony | Synchronous | Asynchronous |
| Optimization | Not possible (it's runtime) | Tree-shaking is 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 at load and compile time
CommonJS:
-
The
require()line runs → the file is read → the code executes. -
So the path can be a variable:
javascriptconst lib = require(condition ? './a' : './b');This works, because it's a plain function call.
ES Modules:
-
Imports must sit at the top of the file and cannot be dynamic:
javascriptimport x from './a'; // fine if (cond) import x from './b'; // an error -
Node.js and the browser know the dependencies in advance, and can load them in parallel before running the code.
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 you get:
requiremodule.exports__dirname__filename
ESM has none of this:
import.meta.urlinstead of__dirnamerequireisn't available
6. Caching behavior
In CommonJS, a module runs once and gets cached:
const a = require('./a');
const b = require('./a');
console.log(a === b); // trueIn ESM, there's also caching, but imports stay live (if the exported variables are let/const):
import { counter } from './a.js';If the module updates the value, every import sees the update.
7. Tree-shaking and optimization
Tree-shaking removes unused code (in Webpack, Rollup, Vite, and so on).
- 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 | Great | Great (Node 13+) |
| Browsers | No | Yes |
| Tree-shaking | No | Yes |
| Top-level await | No | Yes |
9. "Live" vs "dead" imports (an 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 binding)10. Conceptually: the key differences
| Parameter | CommonJS | ESM |
|---|---|---|
| Foundation | Node.js (server) | The ECMAScript standard |
| Execution model | Runs immediately (runtime) | Analyzes dependencies first |
| Imports | Allowed anywhere | Top level only |
| Environment access | 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 run at startup. ES Modules is a static, asynchronous, standardized system, where dependencies are known in advance and an import creates a live binding.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.