Skip to main content

ESM vs CommonJS

1. In short: the core idea

SystemCore 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:

javascript
// math.js module.exports = { x: 1 }; // app.js const math = require('./math'); console.log(math.x); // 1 math.x = 2; // changes only the local copy

ES 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:

javascript
// 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

ConceptCommonJSES Modules
When it loadsAt runtimeBefore execution (parse time)
ImportsDynamic (require())Static (import)
Binding typeValues are copiedLive bindings
Execution orderTop to bottomDetermined by dependencies
AsynchronySynchronousAsynchronous
OptimizationNot possible (it's runtime)Tree-shaking is possible
Contextthis = module.exportsthis = undefined
CachingAfter the first callYes, but with "live" values
EcosystemNode.js, older packagesModern JS, browsers, Node 13+
ArchitectureImperativeDeclarative
CompatibilityServer onlyUniversal (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:

    javascript
    const 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:

    javascript
    import 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:

javascript
(function (exports, require, module, __filename, __dirname) { // your code });

So you get:

  • require
  • module.exports
  • __dirname
  • __filename

ESM has none of this:

  • import.meta.url instead of __dirname
  • require isn't available

6. Caching behavior

In CommonJS, a module runs once and gets cached:

javascript
const a = require('./a'); const b = require('./a'); console.log(a === b); // true

In ESM, there's also caching, but imports stay live (if the exported variables are let/const):

javascript
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:

javascript
import { used } from './utils.js'; // the bundler knows only "used" is needed

8. Compatibility

ScenarioCommonJSES Modules
Using older packagesYesSometimes needs createRequire()
SSR / serverGreatGreat (Node 13+)
BrowsersNoYes
Tree-shakingNoYes
Top-level awaitNoYes

9. "Live" vs "dead" imports (an example)

CommonJS:

javascript
// 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:

javascript
// 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

ParameterCommonJSESM
FoundationNode.js (server)The ECMAScript standard
Execution modelRuns immediately (runtime)Analyzes dependencies first
ImportsAllowed anywhereTop level only
Environment accessrequire, module, exports, __dirnameimport.meta.url, import/export
CompatibilityMany older librariesModern bundlers and browsers
PerformanceFaster for local requiresMore 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.