CommonJS vs ES Modules

CommonJS (CJS) и ES Modules (ESM) — две системы модулей JavaScript: CommonJS использует require/module.exports и применяется в Node.js, ES Modules (import/export) — стандарт ECMAScript, поддерживаемый браузерами и современным Node.js.

Зачем нужно

Понимание различий между CJS и ESM критично при настройке сборщиков (webpack, Vite, esbuild), публикации npm-пакетов (dual package) и использовании нативных ES-модулей в браузере. Наиболее частая проблема: пакет написан на ESM ("type": "module"), а проект — на CJS, или наоборот. Tree shaking работает только с ESM, что напрямую влияет на размер бандла.

Где используется

  • Публикация npm-пакетов: dual CJS+ESM для максимальной совместимости
  • Node.js проекты: CJS (по умолчанию) или ESM (через "type": "module")
  • Браузерные ES Modules: <script type="module">, нативный import без бандлера
  • Vite/webpack: ESM нативно, CJS через трансформацию
  • Dynamic imports: import (ESM) — ленивая загрузка модулей

Основной контент

CommonJS

// Экспорт — math.js
const add = (a, b) => a + b;
const sub = (a, b) => a - b;

module.exports = { add, sub };
// или
module.exports.PI = 3.14159;
exports.square = (n) => n * n; // shorthand

// Импорт — main.js
const { add, sub } = require('./math');
const math = require('./math'); // весь модуль
console.log(math.PI);

// Динамический require (синхронный!)
const config = require(`./config/${process.env.NODE_ENV}`);

ES Modules

// Экспорт — math.js
export const add = (a, b) => a + b;
export const sub = (a, b) => a - b;
export const PI = 3.14159;

// Дефолтный экспорт
export default function multiply(a, b) { return a * b; }

// Импорт — main.js
import { add, sub } from './math.js'; // расширение обязательно в ESM!
import multiply, { PI } from './math.js'; // default + named
import * as math from './math.js'; // весь модуль как namespace

// Динамический import (асинхронный, возвращает Promise)
const { add } = await import('./math.js');
// или ленивая загрузка
button.addEventListener('click', async () => {
  const { heavyModule } = await import('./heavy.js');
  heavyModule.init;
});

// Re-export
export { add, sub } from './math.js';
export { default as multiply } from './math.js';

Ключевые различия

// 1. Синхронность vs асинхронность
// CJS: require — синхронный, блокирующий
// ESM: import — статический (анализируется до выполнения)

// 2. Статический анализ и tree shaking
// CJS: require внутри условий → нельзя анализировать статически
if (condition) { const mod = require('./mod'); } // нет tree shaking

// ESM: import всегда на верхнем уровне → tree shaking работает
import { specificFunction } from './utils.js'; // bundler уберёт лишнее

// 3. this в корне модуля
// CJS: this === module.exports (объект)
// ESM: this === undefined в строгом режиме

// 4. __dirname, __filename — нет в ESM
// Замена в ESM:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// 5. package.json для выбора типа
// { "type": "module" }  — .js файлы считаются ESM
// { "type": "commonjs" } — .js файлы считаются CJS (по умолчанию)
// .mjs — всегда ESM, .cjs — всегда CJS

Dual package (CJS + ESM)

{
  "name": "my-package",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

Частые ошибки

  • Забыть расширение .js в ESM: import './utils' работает в CJS, но в ESM требует явного расширения — import './utils.js'.
  • Использование require в ESM-файле: ERR_REQUIRE_ESM — нельзя require ESM-пакет из CJS без специального workaround.
  • Круговые зависимости: в CJS получаете частично инициализированный объект; в ESM — "live binding" разрешает проблему, но всё равно лучше избегать.
  • __dirname в ESM: __dirname не существует в ESM — используйте import.meta.url + fileURLToPath.

Связанные темы

Ресурсы