Ограничения splitting: только ESM

Механизм разделения кода (code splitting) в esbuild построен вокруг строго определённой модели модулей. Единственный поддерживаемый сценарий для генерации разделённых чанков — использование формата ESM (ECMAScript Modules).

Любая попытка использовать code splitting в других форматах приводит к фактическому отключению этой возможности на уровне архитектуры сборщика.


Требование ESM как фундаментальное ограничение

Code splitting в esbuild существует только в связке:

  • format: "esm"
  • splitting: true

При любом другом формате поведение меняется радикально:

  • format: "cjs" → разделение кода отключается полностью
  • format: "iife" → разделение кода невозможно концептуально
  • смешанные режимы → недопустимы

Это ограничение не является искусственным, оно связано с тем, как формируются графы зависимостей и как осуществляется ленивое подключение модулей.


Причина ограничения: модель исполнения модулей

ESM обладает встроенной поддержкой:

  • статического анализа импортов
  • асинхронной загрузки через import()
  • разделения графа на независимые части
  • предсказуемого связывания зависимостей

Пример базовой структуры:

import { render } from "./view.js";

export function start() {
  render();
}

И динамическая граница разделения:

button.addEventListener("click", async () => {
  const module = await import("./heavy.js");
  module.run();
});

Именно import() создаёт естественную точку разрыва, из которой esbuild формирует отдельный чанк.


Что происходит при включении splitting

При splitting: true esbuild выполняет следующие шаги:

  • строит единый граф модулей
  • выявляет динамические границы (import())
  • анализирует общие зависимости между entry points
  • выделяет shared chunks
  • генерирует несколько файлов вместо одного

Пример конфигурации:

import { build } from "esbuild";

build({
  entryPoints: ["src/app.js"],
  outdir: "dist",
  bundle: true,
  format: "esm",
  splitting: true
});

Почему CommonJS несовместим с splitting

Модульная система CommonJS:

  • выполняется синхронно
  • использует require()
  • не имеет статического графа импортов
  • допускает динамическое изменение зависимостей во время исполнения

Пример:

const mod = require("./mod.js");

Проблема заключается в том, что:

  • невозможно заранее определить границы чанков
  • отсутствует механизм ленивой загрузки как часть стандарта
  • порядок выполнения кода может быть изменён во время runtime

Поэтому при format: "cjs" esbuild полностью отключает splitting.


Ограничение: только браузерно-ориентированная модель ESM

Хотя ESM поддерживается в Node.js, реализация splitting в esbuild ориентирована на:

  • браузерный загрузчик модулей
  • HTTP-based chunk loading
  • динамическую подгрузку файлов

В результате:

  • выходные чанки предполагают наличие import-поддержки окружения
  • требуется runtime, способный обрабатывать ESM динамически

Влияние splitting на output-граф

При включённом splitting структура выходных файлов меняется:

  • entry chunk содержит только стартовый код
  • shared dependencies выносятся в отдельные файлы
  • динамические импорты становятся точками загрузки чанков

Пример условной структуры:

dist/
  app.js
  chunk-A.js
  chunk-B.js
  shared.js

Ограничение на синхронные API загрузки

ESM-based splitting исключает:

  • синхронную загрузку модулей
  • использование require() в рантайме
  • блокирующие импорты ресурсов

Любой код, зависящий от синхронного исполнения модулей, нарушает модель разделения.


Взаимодействие splitting и tree shaking

Code splitting тесно связан с tree shaking:

  • tree shaking удаляет неиспользуемый код на уровне модулей
  • splitting распределяет оставшийся код по чанкам

Однако важное ограничение:

  • tree shaking работает только при ESM
  • при CJS tree shaking существенно ограничен или отсутствует
  • splitting невозможен без tree shaking-совместимого графа

Динамические import() как единственная точка разделения

Единственный поддерживаемый механизм создания ленивых границ:

async function loadFeature() {
  const feature = await import("./feature.js");
  feature.init();
}

Любые попытки имитации splitting через:

  • условные require
  • runtime-ветвления модулей
  • фабрики зависимостей

не создают физических чанков при сборке.


Общие зависимости и shared chunks

ESM-модель позволяет esbuild автоматически выявлять:

  • повторно используемые модули
  • пересечения между entry points
  • общие библиотеки

И выносить их в shared chunk без ручной настройки.

Пример:

// entry-a.js
import { util } from "./util.js";

// entry-b.js
import { util } from "./util.js";

Результат:

  • util.js становится отдельным чанком
  • оба entry используют один общий файл

Ограничения графа при циклических зависимостях

Циклы в ESM:

  • допускаются спецификацией
  • требуют статического разрешения ссылок

При splitting это приводит к:

  • увеличению количества runtime-обёрток
  • необходимости сохранения live bindings
  • усложнению порядка инициализации модулей

esbuild сохраняет корректность, но не оптимизирует циклы агрессивно, чтобы не нарушить семантику ESM.


Отсутствие пользовательского контроля над чанками

В отличие от некоторых сборщиков, esbuild:

  • не предоставляет API для ручного назначения chunk names
  • не поддерживает произвольные стратегии grouping
  • не реализует rollup-style manualChunks

Единственные факторы влияния:

  • структура import/export
  • динамические import()
  • entryPoints

Runtime-следствия ESM splitting

Выходной код требует окружения, поддерживающего:

  • <script type="module"> в браузере
  • ESM loader в Node.js
  • корректную обработку относительных URL модулей

Нарушение этих условий приводит к:

  • невозможности загрузки чанков
  • ошибкам разрешения модулей
  • потере динамического разделения

Итоговая модель ограничения

Splitting в esbuild можно формализовать как:

  • ESM → разрешено
  • CJS → запрещено
  • IIFE → невозможно

И вся архитектура code splitting опирается на одно условие:

наличие стандартизированного механизма асинхронной загрузки модулей на уровне языка