В esbuild разбиение кода на чанки строится вокруг графа модулей и простого набора эвристик, ориентированных на скорость компиляции. В отличие от более сложных бандлеров, здесь нет полноценной системы ручного управления чанками или детальной оптимизации по стратегиям кеширования — решение принимается автоматически на основе структуры импортов, точек входа и динамических границ загрузки.
В основе работы esbuild лежит построение направленного графа
зависимостей. Каждый файл рассматривается как узел, а
import или require (в режиме совместимости)
создают ребра к другим модулям.
При включении опции splitting: true граф начинает
анализироваться не только как единый поток зависимостей для одного
бандла, но как совокупность потенциально независимых подграфов, которые
можно вынести в отдельные файлы.
Ключевое наблюдение: esbuild не пытается «понимать архитектуру приложения», он лишь определяет:
Если задано несколько entry points, esbuild сначала строит полный граф зависимостей для каждого из них. Далее происходит пересечение этих графов.
Модули делятся на три категории:
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-файлов.
Если модуль встречается:
он выносится в отдельный файл, а в исходных чанках остаётся ссылка на него.
Фактически формируется система:
import() является важнейшим маркером разделения. Esbuild
трактует его как явную границу асинхронного чанка.
button.oncl ick = async () => {
const module = await import("./heavy-chart.js");
module.renderChart();
};
Здесь heavy-chart.js и все его зависимости становятся
отдельным чанком.
Важно, что esbuild не смешивает динамические чанки с синхронным графом даже при наличии общих зависимостей. Вместо этого он может вынести общие зависимости ещё выше — в shared chunk.
Решение о создании отдельного чанка опирается на частоту и контекст использования модуля:
Эта иерархия важна: async чанки имеют приоритет над shared, так как требуют отдельной загрузки.
Перед формированием чанков esbuild выполняет удаление неиспользуемого кода. Это влияет на финальный состав модулей внутри чанка.
Если часть экспортов модуля не используется ни в одном месте графа, она исключается ещё до этапа группировки.
// utils.js
export function used() {}
export function unused() {}
Если unused нигде не импортируется, она не влияет на
размер ни одного чанка и фактически исчезает из графа.
Таким образом, чанки формируются уже на «очищенном» графе.
Опция metafile: true позволяет получить детальную карту
того, как именно esbuild распределил модули по чанкам.
В метафайле фиксируются:
Это особенно важно для понимания того, почему конкретный модуль оказался в отдельном файле.
Если splitting: false, весь граф схлопывается в один
бандл на каждый entry point. При этом:
Таким образом, механизм чанкинга полностью отключается и остаётся только простая компоновка.
Архитектура esbuild накладывает ряд ограничений:
manualChunks);Это приводит к тому, что поведение системы предсказуемо, но менее гибко по сравнению с более тяжёлыми бандлерами.
В сложных графах один модуль может участвовать сразу в нескольких уровнях:
В таком случае esbuild поднимает его на уровень, обеспечивающий минимальное дублирование — обычно в shared chunk верхнего уровня. Дальнейшие зависимости могут каскадно перемещаться выше, если они также оказываются общими.
Чанки формируются не изолированно, а каскадно:
Этот процесс приводит к тому, что структура output-файлов отражает не только зависимости, но и их пересечения.
Внутри модели esbuild каждый импорт рассматривается как минимальная единица влияния на структуру чанков. Увеличение количества entry points или динамических импортов прямо увеличивает количество потенциальных split-границ.
При этом сам модуль не «знает», в каком контексте он будет использован — решение всегда принимается сверху графа.
Если один и тот же файл импортируется:
esbuild может создать два разных представления зависимости:
При этом фактический код может быть дедуплицирован на уровне runtime или вынесен в отдельный shared chunk в зависимости от структуры графа.
Имена выходных файлов обычно содержат хеш содержимого. Это означает, что изменение даже одного импортируемого модуля приводит к изменению имени чанка, в который он попал.
Поэтому стратегия разбиения напрямую влияет на стабильность кеша:
Поведение esbuild при формировании чанков можно свести к следующей схеме:
import();Эта модель минималистична, но обеспечивает высокую скорость сборки даже на больших проектах, сохраняя предсказуемую структуру выходных файлов.