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 — нельзяrequireESM-пакет из CJS без специального workaround. - Круговые зависимости: в CJS получаете частично инициализированный объект; в ESM — "live binding" разрешает проблему, но всё равно лучше избегать.
__dirnameв ESM:__dirnameне существует в ESM — используйтеimport.meta.url+fileURLToPath.