Чем отличается CommonJS от ESM в Node.js?

MiddleNode.js · Backend·Обновлено 10 июля 2026
Коротко
CommonJS — это синхронная модульная система Node.js с require/module.exports, загружающая модули во время выполнения. ESM (ECMAScript Modules) — стандарт языка с import/export, поддерживающий статический анализ и асинхронную загрузку.

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 файлов в пакете

Лучшие курсы по теме

изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Node.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее