ESM (ECMAScript Modules) — стандартная модульная система JavaScript, официально включённая в спецификацию ECMAScript. Она пришла на смену множеству альтернативных решений, таких как CommonJS, AMD и UMD, и стала единым механизмом организации кода как в браузерах, так и в серверной среде.
Модуль представляет собой отдельный файл с собственным пространством имён. Переменные, функции и классы, объявленные внутри модуля, не попадают в глобальную область видимости автоматически. Для обмена функциональностью между файлами используются конструкции export и import.
Простейший пример экспорта:
// math.js
export function sum(a, b) {
return a + b;
}
Импорт в другом модуле:
// app.js
import { sum } from "./math.js";
console.log(sum(2, 3));
Основная идея ESM заключается в явном описании зависимостей между файлами. Благодаря этому инструменты сборки, включая Esbuild, способны эффективно анализировать структуру проекта и выполнять оптимизации.
До появления ES-модулей каждая среда использовала собственную систему модулей.
Использовался в Node.js:
const math = require("./math");
console.log(math.sum(2, 3));
Экспорт:
module.exports = {
sum
};
Применялся преимущественно в браузерах:
define(["math"], function(math) {
console.log(math.sum(2, 3));
});
ESM решил эти проблемы благодаря единому синтаксису и статической структуре импортов.
Esbuild создавался уже в эпоху распространения ES-модулей, поэтому ESM является для него естественным форматом работы.
При обработке проекта Esbuild:
Пример запуска:
esbuild src/index.js --bundle --outfile=dist/app.js
Если входной файл содержит ESM-импорты:
import { sum } from "./math.js";
console.log(sum(1, 2));
Esbuild автоматически найдёт все зависимости и включит их в сборку.
Наиболее распространённый вариант работы с ESM — именованный экспорт.
Файл:
// utils.js
export function formatDate(date) {
return date.toISOString();
}
export function formatPrice(price) {
return `$${price}`;
}
Импорт:
import { formatDate, formatPrice } from "./utils.js";
Преимущества:
Функции можно экспортировать после объявления:
function sum(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
export { sum, subtract };
Подобный стиль часто используется в библиотеках, где экспортируемый интерфейс собирается в конце файла.
ESM позволяет задавать другое имя экспортируемой сущности.
function formatDate(date) {
return date.toISOString();
}
export {
formatDate as format
};
Импорт:
import { format } from "./utils.js";
Такой механизм помогает формировать более удобный публичный API.
Модуль может иметь один экспорт по умолчанию.
export default function createLogger() {
return {
log(message) {
console.log(message);
}
};
}
Импорт:
import createLogger from "./logger.js";
Название при импорте выбирается произвольно:
import loggerFactory from "./logger.js";
Это отличает default export от именованных экспортов.
Оба подхода могут использоваться совместно.
export default class User {}
export const VERSION = "1.0.0";
export const AUTHOR = "Admin";
Импорт:
import User, { VERSION, AUTHOR } from "./user.js";
Иногда требуется получить объект со всеми экспортами модуля.
import * as utils from "./utils.js";
console.log(utils.formatDate(new Date()));
console.log(utils.formatPrice(100));
После импорта:
utils.formatDate(...)
utils.formatPrice(...)
Такой подход удобен для больших наборов функций, но может снижать эффективность tree shaking.
ESM позволяет перенаправлять экспорты.
export { sum } from "./math.js";
export * from "./math.js";
export * from "./math.js";
export * from "./string.js";
export * from "./date.js";
Этот подход широко используется при создании публичных API библиотек.
Часто создаётся файл:
// index.js
С содержимым:
export * from "./math.js";
export * from "./string.js";
export * from "./date.js";
Использование:
import { sum, capitalize } from "./index.js";
В крупных проектах индексные файлы позволяют скрывать внутреннюю структуру каталогов.
ESM поддерживает ленивую загрузку модулей.
const module = await import("./analytics.js");
Возвращается Promise:
import("./analytics.js")
.then(module => {
module.track();
});
Преимущества:
Esbuild умеет анализировать динамические импорты и формировать отдельные чанки.
Пример:
async function openAdminPanel() {
const admin = await import("./admin.js");
admin.start();
}
Сборка:
esbuild src/index.js \
--bundle \
--splitting \
--format=esm \
--outdir=dist
Параметр:
--splitting
включает code splitting.
В результате создаются отдельные файлы:
dist/
├── index.js
├── admin-XYZ.js
Основной файл загружается сразу, а административный модуль — только при необходимости.
Одно из ключевых преимуществ ESM — возможность статического анализа.
Корректный импорт:
import { sum } from "./math.js";
Некорректный динамический вариант:
const name = "math";
require("./" + name);
Во втором случае сборщику значительно сложнее понять структуру зависимостей.
Благодаря фиксированному синтаксису ESM Esbuild способен заранее определить:
Tree Shaking — удаление неиспользуемого кода.
Модуль:
export function sum(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
export function divide(a, b) {
return a / b;
}
Использование:
import { sum } from "./math.js";
console.log(sum(1, 2));
Во время сборки Esbuild может исключить:
multiply
divide
из итогового бандла.
Это возможно именно благодаря статической природе ES-модулей.
Особенность ESM — живые ссылки на экспортируемые значения.
Модуль:
export let counter = 0;
export function increment() {
counter++;
}
Импорт:
import { counter, increment } from "./counter.js";
console.log(counter);
increment();
console.log(counter);
Вывод:
0
1
Импортируемая переменная автоматически отражает изменения экспортируемого значения.
В CommonJS такое поведение отличается.
ESM способен работать с циклическими зависимостями.
Файл A:
import { b } from "./b.js";
export const a = "A";
Файл B:
import { a } from "./a.js";
export const b = "B";
Esbuild корректно анализирует подобные случаи, однако большое количество циклических зависимостей обычно считается архитектурным недостатком.
Рекомендуется минимизировать такие связи между модулями.
Современные браузеры поддерживают ES-модули напрямую.
<script type="module" src="app.js"></script>
Внутри файла:
import { sum } from "./math.js";
console.log(sum(2, 3));
Особенности:
Node.js поддерживает ESM несколькими способами.
{
"type": "module"
}
После этого:
import fs from "fs";
становится допустимым синтаксисом.
app.mjs
Node автоматически интерпретирует файл как ES-модуль.
Esbuild способен преобразовывать модули в разные форматы.
esbuild src/index.js \
--bundle \
--format=esm \
--outfile=dist/app.js
Выходной код остаётся ES-модульным.
esbuild src/index.js \
--bundle \
--format=cjs \
--outfile=dist/app.js
Используется для совместимости со старым окружением Node.js.
esbuild src/index.js \
--bundle \
--format=iife \
--global-name=App \
--outfile=dist/app.js
Подходит для подключения через обычный тег
<script>.
ESM использует относительные пути.
import { sum } from "./math.js";
Импорт из родительской директории:
import { sum } from "../utils/math.js";
Импорт пакета:
import React from "react";
Esbuild повторяет стандартную логику поиска модулей и дополнительно умеет оптимизировать процесс разрешения зависимостей.
Современные библиотеки часто публикуют ESM-сборки.
Пример структуры:
package/
├── dist/
│ ├── index.js
│ ├── index.cjs
│ └── index.d.ts
└── package.json
Конфигурация:
{
"main": "./dist/index.cjs",
"module": "./dist/index.js"
}
Либо через поле exports:
{
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}
Esbuild корректно обрабатывает подобные настройки и выбирает подходящую версию пакета.
export function fetchUser() {}
export function saveUser() {}
Они лучше взаимодействуют с tree shaking.
Большие проекты становятся сложнее для навигации, когда большинство файлов экспортирует значения по умолчанию.
const chart = await import("./charts.js");
Это уменьшает объём первоначальной загрузки.
export * from "./api.js";
export * from "./models.js";
export * from "./utils.js";
Небольшие специализированные модули обычно легче оптимизируются Esbuild и обеспечивают более эффективное удаление неиспользуемого кода.
Esbuild достигает высокой скорости благодаря сочетанию нескольких факторов:
Чем последовательнее проект использует нативные ES-модули, тем больше возможностей получает Esbuild для оптимизации итогового результата. ESM становится не только стандартом организации кода, но и фундаментом для современных механизмов сборки, минимизации и доставки JavaScript-приложений.