Перейти к содержимому

CommonJS vs ESM

CJS и ESM имеют фундаментально разную семантику: CJS — динамический runtime require с копированием значений, ESM — статический compile-time граф с live bindings — это объясняет разницу в tree-shaking, circular deps и interop.

a.cjs
// CommonJS: динамический, синхронный, кэшируется по realpath
const b = require('./b.cjs'); // синхронный вызов
console.log('a loaded');
module.exports = { name: 'a', bName: b.name };
// require() можно вызывать условно / в функции:
if (process.env.DEBUG) {
const debug = require('./debug'); // lazy loading — OK в CJS
}
// Circular deps в CJS: частично заполненный объект (не ошибка!)
// a.cjs: exports.fn = () => b.fn()
// b.cjs: const a = require('./a'); → a = {} (пустой на момент вызова!)
// → тихий баг: a.fn === undefined во время инициализации b
// ESM: статический, async, live bindings
// Статические imports анализируются ДО выполнения кода:
import { name } from './b.mjs';
// Нельзя: if (cond) import { x } from '...' ← SyntaxError
// Для динамики: const m = await import('./b.mjs')
// Live bindings — ключевое отличие от CJS:
// counter.mjs:
export let count = 0;
export const increment = () => count++;
// main.mjs:
import { count, increment } from './counter.mjs';
console.log(count); // 0
increment();
console.log(count); // 1 ← live binding! count — это alias, не копия
// В CJS: const { count } = require('./counter') → копия примитива → всегда 0
// Interop CJS ↔ ESM в Node.js
// ESM может импортировать CJS (только default export):
import cjsModule from './lib.cjs'; // cjsModule = module.exports
// Named imports из CJS: экспериментально (Node 22+ --experimental-require-module)
// CJS НЕ может require() ESM синхронно:
// require('./esm.mjs') → ERR_REQUIRE_ESM
// Только через dynamic import():
async function loadEsm() {
const { fn, default: main } = await import('./esm.mjs');
return main;
}
// Dual package паттерн (правильный способ):
// package.json:
// { "exports": {
// "import": "./dist/index.mjs", // для ESM
// "require": "./dist/index.cjs" // для CJS
// } }

Итог: CJS — динамический require с копированием значений; ESM — статический граф с live bindings; различие в семантике экспортов объясняет почему tree-shaking работает только с ESM и почему circular deps ведут себя по-разному.