Общие чанки: как esbuild решает, что выносить

В esbuild разбиение кода на чанки строится вокруг графа модулей и простого набора эвристик, ориентированных на скорость компиляции. В отличие от более сложных бандлеров, здесь нет полноценной системы ручного управления чанками или детальной оптимизации по стратегиям кеширования — решение принимается автоматически на основе структуры импортов, точек входа и динамических границ загрузки.

В основе работы esbuild лежит построение направленного графа зависимостей. Каждый файл рассматривается как узел, а import или require (в режиме совместимости) создают ребра к другим модулям.

При включении опции splitting: true граф начинает анализироваться не только как единый поток зависимостей для одного бандла, но как совокупность потенциально независимых подграфов, которые можно вынести в отдельные файлы.

Ключевое наблюдение: esbuild не пытается «понимать архитектуру приложения», он лишь определяет:

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

Точки входа и первичное разбиение

Если задано несколько entry points, esbuild сначала строит полный граф зависимостей для каждого из них. Далее происходит пересечение этих графов.

Модули делятся на три категории:

  • уникальные для конкретного entry point;
  • общие для нескольких entry points;
  • лениво загружаемые через import().

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

Пример:

// entry-a.js
import { format } from "./utils/format.js";
import { renderA } from "./viewA.js";

// entry-b.js
import { format } from "./utils/format.js";
import { renderB } from "./viewB.js";

Модуль format.js становится общим и почти неизбежно попадает в отдельный чанк, потому что используется в нескольких независимых графах.

Общие чанки и принцип дедупликации

Основной механизм формирования общих чанков основан на дедупликации модулей. Esbuild стремится не включать один и тот же модуль в несколько output-файлов.

Если модуль встречается:

  • в двух и более entry points;
  • или в entry point и в динамическом импорте,

он выносится в отдельный файл, а в исходных чанках остаётся ссылка на него.

Фактически формируется система:

  • entry chunk — минимальный загрузочный слой;
  • shared chunk — пересечение зависимостей;
  • dynamic chunk — лениво загружаемые части.

Динамические импорты как границы чанков

import() является важнейшим маркером разделения. Esbuild трактует его как явную границу асинхронного чанка.

button.oncl ick = async () => {
  const module = await import("./heavy-chart.js");
  module.renderChart();
};

Здесь heavy-chart.js и все его зависимости становятся отдельным чанком.

Важно, что esbuild не смешивает динамические чанки с синхронным графом даже при наличии общих зависимостей. Вместо этого он может вынести общие зависимости ещё выше — в shared chunk.

Алгоритм принятия решения о выносе

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

  1. Если модуль используется только в одном месте — он инлайнится в соответствующий чанк.
  2. Если используется в нескольких entry points — выносится в shared chunk.
  3. Если используется в динамическом импорте — выносится в отдельный async chunk.
  4. Если пересекается несколько условий — применяется правило наибольшей «широты доступа» (обычно async > shared > local).

Эта иерархия важна: async чанки имеют приоритет над shared, так как требуют отдельной загрузки.

Влияние tree-shaking на чанки

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

Если часть экспортов модуля не используется ни в одном месте графа, она исключается ещё до этапа группировки.

// utils.js
export function used() {}
export function unused() {}

Если unused нигде не импортируется, она не влияет на размер ни одного чанка и фактически исчезает из графа.

Таким образом, чанки формируются уже на «очищенном» графе.

Роль метаданных и метафайла

Опция metafile: true позволяет получить детальную карту того, как именно esbuild распределил модули по чанкам.

В метафайле фиксируются:

  • какие входные файлы породили какие выходные чанки;
  • какие модули вошли в каждый чанк;
  • какие зависимости были перенесены в shared слой.

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

Поведение при отсутствии splitting

Если splitting: false, весь граф схлопывается в один бандл на каждый entry point. При этом:

  • динамические импорты становятся точками создания отдельных файлов только в ESM-режиме;
  • shared chunks не формируются;
  • каждый entry point получает собственный независимый bundle.

Таким образом, механизм чанкинга полностью отключается и остаётся только простая компоновка.

Ограничения модели чанков

Архитектура esbuild накладывает ряд ограничений:

  • отсутствует ручное управление чанками (нет аналога manualChunks);
  • невозможна тонкая настройка стратегий кеширования;
  • нет приоритизации «веса» модулей;
  • нет анализа runtime-поведения, только статический граф.

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

Общие зависимости нескольких уровней

В сложных графах один модуль может участвовать сразу в нескольких уровнях:

  • импортироваться в entry A;
  • импортироваться в entry B;
  • быть частью динамического импорта внутри entry A.

В таком случае esbuild поднимает его на уровень, обеспечивающий минимальное дублирование — обычно в shared chunk верхнего уровня. Дальнейшие зависимости могут каскадно перемещаться выше, если они также оказываются общими.

Каскадное формирование чанков

Чанки формируются не изолированно, а каскадно:

  1. Сначала определяется полный граф.
  2. Затем выделяются динамические подграфы.
  3. Затем вычисляются пересечения entry points.
  4. Затем общие зависимости поднимаются выше по иерархии.

Этот процесс приводит к тому, что структура output-файлов отражает не только зависимости, но и их пересечения.

Импорт как единица стоимости

Внутри модели esbuild каждый импорт рассматривается как минимальная единица влияния на структуру чанков. Увеличение количества entry points или динамических импортов прямо увеличивает количество потенциальных split-границ.

При этом сам модуль не «знает», в каком контексте он будет использован — решение всегда принимается сверху графа.

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

Если один и тот же файл импортируется:

  • синхронно в одном месте;
  • асинхронно в другом,

esbuild может создать два разных представления зависимости:

  • один экземпляр в shared/sync графе;
  • второй — в async чанке.

При этом фактический код может быть дедуплицирован на уровне runtime или вынесен в отдельный shared chunk в зависимости от структуры графа.

Хеширование и стабильность чанков

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

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

  • мелкие shared chunks увеличивают вероятность изменения хешей;
  • крупные стабильные chunks уменьшают число изменений при обновлениях.

Итоговая модель принятия решений

Поведение esbuild при формировании чанков можно свести к следующей схеме:

  • построение графа модулей;
  • выделение entry points;
  • выделение async границ через import();
  • поиск пересечений зависимостей;
  • вынос общих модулей в shared слой;
  • финальная упаковка с дедупликацией и tree-shaking.

Эта модель минималистична, но обеспечивает высокую скорость сборки даже на больших проектах, сохраняя предсказуемую структуру выходных файлов.