CommonJS vs ESM
Никита БалихинЗа время своего существования Javascript сменил несколько разных модульных систем. На текущий момент актуальными среди них остаются CommonJS и ESM (EcmaScript Modules). Фактически же стандартом являются только ESM: по ним есть спецификация, они нативно поддерживаются в современных браузерах и в Node.js (аж с версии 12.17.0).
В этой статье я расскажу о том, что мне удалось выяснить об этих модульных форматах с практической точки зрения.
CommonJS
CommonJS — модульный формат, который появился в Node.js и никогда не поддерживался в браузерах. Он добавляет в язык глобальную переменную module
и функцию require
.
CommonJS модули:
- Синхронные —
require
всегда выполняется синхронно, приостанавливая выполнение программы на время подключения модуля; - Динамические — вызов
require
и изменениеmodule.exports
могут находиться в любом блоке кода в том числе в условии и цикле.
Синхронность не позволяет делать ленивую загрузку модулей, что ограничивает разработчика в возможностях по оптимизации производительности приложения.
Динамическая природа CommonJS не позволяет сборщикам (например, Webpack) однозначно построить дерево зависимостей в приложении и удалить неиспользуемый код из итогового бандла (Tree Shaking), что негативно скажется на его размере.
ESM
EcmaScript Modules — модульный формат, который в первую очередь появился в виде спецификации, затем был реализован в браузерах и впоследствии также появился в Node.js. ESM привносит в Javascript ключевые слова import
и export
.
EcmaScript модули:
- Асинхронные —
import
не приостанавливает выполнение программы (но в Node.js они по прежнему синхронные); - Статические — операторы
import
иexport
могут находиться только на верхнем уровне модуля, что исключает возможность делать условные связи между модулями.
Казалось бы ввиду описанных выше свойств ES модулей возможности разработчика ограничиваются, поэтому существует асинхронная функция import
, осуществляющая динамическое подключение модуля.
Асинхронность позволяет браузеру загружать ES модули параллельно, что актуально со времён появления протокола HTTP/2, поддерживающего мультиплексирование запросов (если, конечно, приложение поставляется пользователю в виде нативных ES модулей, а не бандлом).
Статическая природа ESM позволяет сборщику однозначно построить дерево зависимостей приложения и избавиться от лишнего кода. Динамические же импорты будут вынесены сборщиком в отдельные чанки вне основного бандла, поэтому они не сказываются на работе механизма Tree Shaking.
Поддержка ESM
EcmaScript модули на текущий момент имеют достаточно широкую поддержку. Опенсорс разработчик Sindre Sorhus (в вашем package-lock.json наверняка найдутся find-up или chalk — это всё его разработки) с определённого момента решил толкать экосистему Javascript в сторону ESM достаточно радикальным образом: он начал публиковать свои пакеты исключительно в ESM формате и написал инструкцию по переходу на ESM.
Тем не менее среди популярных инструментов есть, например, Jest, поддержка ESM в котором на текущий момент экспериментальная, поэтому при разработке библиотек для максимально безболезненного использования я бы всё-таки генерировал фоллбэк в виде сборки в CommonJS.
Использование ESM в Node.js
Для использования ESM пакетов в Node.js ваше приложение должно тоже быть в формате ESM, так как подключить ES модуль в CommonJS нельзя, а вот наоборот — вполне возможно.
Добиться этого можно двумя способами:
- Расширение
.mjs
(для Javascript) или.mts
(для Typescript) у файлов "type": "module"
вpackage.json
В импортах в настоящих ES модулях при этом нужно явно указывать расширение файлов.
Заключение
ESM — будущее (и отчасти настоящее) экосистемы Javascript. По возможности разработку стоит вести именно в этом модульном формате, так как он имеет ряд объективных преимуществ и имеет "future proof" в виде спецификации, но на текущий момент особенно в энтерпрайз разработке не стоит забывать и о CommonJS, поставляя пользователям библиотеки код сразу в обоих модульных форматах для максимальной совместимости.