Skip to main content

ESM vs CommonJS

1. In short: the main idea

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

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

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:

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. Conceptual differences

ConceptCommonJSES Modules
Load timeAt runtimeBefore execution (parse-time)
ImportsDynamic (require())Static (import)
Binding typeValues are copiedLive references (live bindings)
Execution orderTop to bottomDetermined by dependencies
AsynchronySynchronousAsynchronous
OptimizationNot possible (runtime)Tree-shaking 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 in loading and compilation timing

CommonJS:

  • The line require() executes -> the file is read -> the code runs.

  • So the path can be a variable:

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

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

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

So there is:

  • require
  • module.exports
  • __dirname
  • __filename

This is absent in ESM:

  • instead of __dirname -> import.meta.url
  • require is unavailable

6. Caching behavior

In CommonJS, a module executes once and is cached:

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

In ESM there is also a cache, but imports remain live (if the variables are exported as let/const):

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

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

8. Compatibility

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

9. "Live" vs "dead" imports (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 reference)

10. Conceptually: the key differences

ParameterCommonJSESM
BasisNode.js (server)ECMAScript standard
Execution modelExecutes immediately (runtime)Analyzes dependencies first
ImportsAllowed anywhereTop level only
Access to the environmentrequire, 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 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 ready
Premium

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