Опция splitting: включение code splitting

Назначение механизма разделения кода

Code splitting в esbuild предназначен для разбиения итогового бандла на несколько независимых файлов (чанков), которые загружаются по мере необходимости. Основная цель — уменьшение начального размера загрузки приложения и оптимизация времени старта, особенно в SPA и крупных фронтенд-системах.

Механизм активируется через опцию:

  • splitting: true

и работает только в связке с модулем ECMAScript:

  • format: "esm"

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


Условия работы code splitting в esbuild

Для корректного включения разбиения кода необходимо соблюдение нескольких условий:

  • Используется формат esm
  • Входная точка содержит динамические импорты import()
  • Указан флаг splitting: true
  • Режим сборки — bundle: true

Пример базовой конфигурации:

import * as esbuild from "esbuild";

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

При несоблюдении любого из этих условий esbuild либо игнорирует splitting, либо выдаёт ошибку конфигурации.


Роль динамических импортов

Основной триггер разбиения кода — использование import():

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

При сборке esbuild выделяет heavy-module.js в отдельный файл чанка, который загружается только при вызове события.

Это позволяет:

  • уменьшить размер initial bundle
  • отложить загрузку тяжёлых зависимостей
  • ускорить first contentful paint

Механизм формирования чанков

esbuild строит граф зависимостей и анализирует:

  • статические импорты (import ... from)
  • динамические импорты (import())

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

Пример структуры:

app.js
 ├── utils.js
 ├── vendor.js
 └── lazy.js (dynamic import)

Результат сборки:

dist/
 ├── app.js
 ├── utils.js
 ├── vendor.js
 └── lazy-[hash].js

Поведение shared dependencies

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

Пример:

// a.js
import { format } from "./utils";

// b.js
import { format } from "./utils";

При динамическом импорте обоих модулей:

import("./a.js");
import("./b.js");

utils.js будет вынесен в отдельный общий чанк.

Это поведение предотвращает дублирование кода и уменьшает общий размер загрузки.


Ограничения splitting в esbuild

Несмотря на высокую скорость сборки, механизм имеет ряд ограничений:

1. Только ESM

CommonJS не поддерживает разделение кода:

  • format: "cjs" → splitting недоступен

2. Отсутствие runtime-роутера

esbuild не предоставляет собственного загрузчика чанков. Браузер выполняет загрузку через стандартный ESM loader.

3. Нет сложной стратегии chunking

В отличие от Webpack, отсутствуют:

  • ручное управление splitChunks
  • тонкая настройка кеш-групп
  • приоритеты чанков

Связь splitting и tree shaking

Code splitting тесно взаимодействует с tree shaking. При включении:

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

Это приводит к более точному распределению кода между файлами.

export function used() {}
export function unused() {}

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


Асинхронные границы и архитектура приложения

Использование splitting формирует архитектурные границы приложения. Типичные сценарии:

  • ленивые страницы
  • модальные окна
  • тяжёлые библиотеки (например, редакторы)
  • аналитические модули

Пример:

router.on("/dashboard", async () => {
  const dashboard = await import("./pages/dashboard.js");
  dashboard.render();
});

Каждый маршрут становится отдельной точкой загрузки.


Именование чанков

По умолчанию esbuild генерирует имена файлов автоматически. Однако можно управлять этим через:

  • entryNames
  • chunkNames
  • assetNames

Пример:

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

Результат:

dist/chunks/utils-a1b2c3.js

Использование [hash] важно для кеширования в браузере.


Анализ результата сборки через metafile

esbuild позволяет получить структуру чанков через metafile:

esbuild.build({
  entryPoints: ["src/app.js"],
  bundle: true,
  splitting: true,
  format: "esm",
  outdir: "dist",
  metafile: true
}).then(result => {
  require("fs").writeFileSync(
    "meta.json",
    JSON.stringify(result.metafile, null, 2)
  );
});

В metafile содержится:

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

Это используется для анализа бандла и оптимизации архитектуры.


Производительность и влияние splitting

Включение splitting влияет на:

Положительные эффекты

  • уменьшение initial bundle size
  • ускорение загрузки первого экрана
  • параллельная загрузка чанков
  • более эффективное кеширование

Дополнительные издержки

  • рост количества HTTP-запросов
  • увеличение сложности деплоя
  • необходимость корректного CDN-кеширования

Поведение в разработке и продакшене

В dev-режиме splitting сохраняется, но:

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

В production:

minify: true,
splitting: true,
format: "esm"

достигается максимальная оптимизация бандла.


Взаимодействие с внешними зависимостями

При использовании npm-библиотек esbuild может:

  • включать их в основной чанк
  • или выделять в отдельные shared chunks

Пример:

import lodash from "lodash";
import("./chart.js");

lodash может попасть в общий vendor chunk при наличии нескольких точек использования.


Практическая структура приложения с splitting

Типичная структура после сборки:

dist/
 ├── app.js              (entry)
 ├── vendor.js           (общие зависимости)
 ├── route-home.js       (lazy route)
 ├── route-admin.js      (lazy route)
 └── components-xyz.js   (shared chunk)

Такое разбиение обеспечивает:

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

Влияние target и современных браузеров

Опция target влияет на генерацию чанков:

  • более новый target → меньше полифилов → меньше размер чанков
  • старый target → больше вспомогательного кода → увеличенные чанки

Пример:

target: "es2022"

уменьшает объем runtime-обвязки и улучшает эффективность splitting.


Сценарии, где splitting критичен

  • большие SPA с роутингом
  • административные панели
  • редакторы (canvas, rich text)
  • аналитические платформы
  • e-commerce с большим количеством страниц

Взаимодействие splitting с кешированием

Разделение кода усиливает эффективность HTTP-кеширования:

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

Использование хеширования имени чанков:

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