Чем отличается CommonJS от ESM в Node.js?
CommonJS и ESM: ключевые отличия
Node.js исторически использовал CommonJS (CJS) как единственную модульную систему, но начиная с версии 12 получил нативную поддержку ECMAScript Modules (ESM). Между ними существуют фундаментальные различия.
Синтаксис
CommonJS использует функции require() и объект module.exports:
// Экспорт в CommonJS
const helper = require('./helper');
module.exports = { foo, bar };
ESM использует декларативный синтаксис import/export:
// Экспорт в ESM
import helper from './helper.js';
export { foo, bar };
Время разрешения зависимостей
CJS — динамический: require() выполняется в рантайме, можно вызывать внутри условий и функций, путь может быть переменной. ESM — статический: все import анализируются до выполнения кода, что позволяет бандлерам применять tree-shaking и обнаруживать ошибки на этапе разбора.
Синхронность vs. асинхронность
require() блокирует поток и загружает модуль синхронно. ESM поддерживает асинхронную загрузку, в том числе динамический import(), возвращающий Promise.
Top-level await
В ESM-модулях можно использовать await на верхнем уровне без обёртки в async-функцию. В CJS это невозможно.
// Только в ESM
const data = await fetch('https://api.example.com/data').then(r => r.json());
Значение this на верхнем уровне
В CJS this на верхнем уровне модуля равно module.exports. В ESM — undefined.
Переменные dirname и filename
В CJS они доступны автоматически. В ESM их нет, но можно воссоздать через import.meta:
// ESM-аналог __dirname
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Расширения файлов и конфигурация
Node.js определяет формат по:
- расширению файла:
.cjs→ CJS,.mjs→ ESM; - полю
"type"вpackage.json:"type": "module"делает.jsфайлы ESM-модулями.
Совместимость
CJS-модуль можно импортировать из ESM через import, но с ограничениями — именованные экспорты могут не разрешиться статически. Импортировать ESM из CJS через require() нельзя — только через import() как Promise.
Кэширование
Оба формата кэшируют модули после первой загрузки. В CJS кэш доступен через require.cache и может быть изменён программно, в ESM прямого доступа к кэшу нет.
Когда что использовать
- ESM — предпочтительный выбор для новых проектов: статический анализ, tree-shaking, top-level await, стандарт языка.
- CJS — сохраняет актуальность для legacy-кода, библиотек с широкой поддержкой старых Node.js и ситуаций, где нужен динамический
require().
Что хочет услышать интервьюер
Знание синтаксических различий: require/module.exports против import/export
Понимание статического (ESM) vs. динамического (CJS) разрешения зависимостей и их влияния на tree-shaking
Осведомлённость о top-level await как ESM-эксклюзиве
Знание конфигурации через package.json (поле type) и расширения файлов .mjs/.cjs
Понимание ограничений совместимости при смешивании CJS и ESM в одном проекте
Пример: Сравнение синтаксиса CJS и ESM
// --- CommonJS (файл math.cjs или без "type":"module") ---
// Экспорт
function add(a, b) {
return a + b;
}
module.exports = { add };
// Импорт
const { add } = require('./math');
console.log(add(2, 3)); // 5
// Динамический require внутри условия — допустимо в CJS
if (process.env.DEBUG) {
const debug = require('debug');
}
// --- ESM (файл math.mjs или "type":"module" в package.json) ---
// Экспорт
export function add(a, b) {
return a + b;
}
// Импорт — расширение обязательно в Node.js
import { add } from './math.js';
console.log(add(2, 3)); // 5
// Top-level await — только в ESM
const response = await fetch('https://api.example.com');
const data = await response.json();
// __dirname в ESM
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const configPath = join(__dirname, 'config.json');
// Импорт CJS-модуля из ESM
import cjsModule from './legacy.cjs'; // работает
// Именованные экспорты могут потребовать деструктуризации вручную
// Динамический import() — работает в обоих форматах, возвращает Promise
const { readFile } = await import('fs/promises');
Типичные ошибки
Считают, что ESM и CJS полностью взаимозаменяемы — не учитывают ограничения при импорте ESM из CJS через require()
Забывают указывать расширение файла в ESM-импортах (обязательно в Node.js: import './foo.js', а не './foo')
Не знают об отсутствии __dirname/__filename в ESM и не умеют их воссоздать через import.meta.url
Путают динамический import() (возвращает Promise, работает в обоих форматах) со статическим import (только ESM)
Не понимают, что поле "type": "module" в package.json меняет интерпретацию всех .js файлов в пакете


