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

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

Базовая модель нескольких entry points

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

import * as esbuild from 'esbuild';

await esbuild.build({
  entryPoints: [
    'src/admin.js',
    'src/client.js',
    'src/analytics.js'
  ],
  outdir: 'dist',
  bundle: true,
  format: 'esm'
});

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

Включение разбиения общих зависимостей

Ключевым механизмом становится опция splitting. Она активирует вынос общих частей кода в отдельные чанки, но работает только в сочетании с форматом ESM.

await esbuild.build({
  entryPoints: [
    'src/admin.js',
    'src/client.js',
    'src/analytics.js'
  ],
  outdir: 'dist',
  bundle: true,
  splitting: true,
  format: 'esm'
});

При включённом splitting esbuild анализирует пересечение графов зависимостей и выносит повторяющиеся модули в отдельные файлы, которые затем импортируются из каждого entry chunk.

Роль outdir и структура выходных файлов

Использование outdir вместо outfile принципиально важно. При нескольких точках входа esbuild обязан формировать каталог, а не один файл.

Результирующая структура может выглядеть следующим образом:

dist/
  admin.js
  client.js
  analytics.js
  chunk-AB12CD.js
  chunk-X9Y8Z7.js

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

Принцип формирования shared chunks

esbuild не использует заранее заданную стратегию разделения вроде manualChunks (как в других сборщиках). Вместо этого применяется алгоритм:

  1. Построение графа модулей для каждой точки входа
  2. Определение пересечений графов
  3. Выделение узлов, присутствующих более чем в одном графе
  4. Перенос таких узлов в отдельные чанки
  5. Замена импортов на ссылки на новые файлы

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

Пример реального пересечения зависимостей

// src/utils/logger.js
export function log(message) {
  console.log(message);
}

// src/admin.js
import { log } from './utils/logger';
log('admin');

// src/client.js
import { log } from './utils/logger';
log('client');

При сборке с несколькими entry points и splitting: true, модуль logger.js будет вынесен в отдельный chunk, так как он используется минимум в двух графах.

Ограничения ручного управления разбиением

Несмотря на термин «ручное разделение», esbuild не предоставляет прямого управления чанками на уровне пользователя. Отсутствуют механизмы:

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

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

Влияние формата сборки

Поддержка разделения строго ограничена ESM. При использовании format: 'cjs' или iife поведение меняется:

  • splitting игнорируется
  • весь граф инлайнится в каждый entry point
  • общие модули дублируются
await esbuild.build({
  entryPoints: ['src/a.js', 'src/b.js'],
  outdir: 'dist',
  bundle: true,
  splitting: true,
  format: 'cjs'
});

В этом случае результат не будет содержать отдельных чанков.

Детерминированность и кеширование

esbuild стремится к стабильной генерации имён чанков при неизменной структуре зависимостей. Однако изменение даже одного внутреннего модуля может привести к:

  • изменению хеша чанка
  • перераспределению зависимостей
  • изменению набора файлов

Это связано с тем, что чанки формируются на основе контента, а не фиксированной схемы.

Взаимодействие с динамическими импортами

Наличие нескольких entry points не исключает использование import() внутри модулей. В этом случае:

  • динамические импорты становятся отдельными async чанками
  • они не обязательно зависят от entry graph
  • могут пересекаться с общими чанками
// src/client.js
button.oncl ick = async () => {
  const module = await import('./heavy-feature.js');
  module.run();
};

Такой модуль будет вынесен в отдельный файл независимо от других entry points, если он не входит в их статический граф.

Разделение по зонам ответственности

На практике несколько точек входа часто используются для изоляции разных частей приложения:

  • административная панель
  • клиентская часть
  • аналитические инструменты
  • внутренние сервисные скрипты

При этом общие библиотеки (HTTP-клиент, утилиты, форматирование данных) автоматически попадают в shared chunks.

Поведение при частичном пересечении графов

Если два entry points имеют частично пересекающиеся зависимости, esbuild создаёт минимальный набор общих чанков. Например:

  • A и B используют модуль X
  • B и C используют модуль Y
  • A не использует Y
  • C не использует X

В этом случае формируются два независимых shared chunk, а не один общий для всех.

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

Использование нескольких entry points с splitting влияет на:

  • размер initial download
  • количество HTTP-запросов
  • эффективность кеширования

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

Метаинформация сборки

Для анализа структуры разделения можно использовать metafile:

await esbuild.build({
  entryPoints: ['src/admin.js', 'src/client.js'],
  outdir: 'dist',
  bundle: true,
  splitting: true,
  format: 'esm',
  metafile: true
});

Метаданные позволяют увидеть:

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

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

Конфигурационные паттерны

Типичная конфигурация для нескольких entry points часто дополняется:

  • entryNames — управление именами выходных файлов
  • chunkNames — шаблон для имен shared chunks
  • assetNames — управление статическими ресурсами
await esbuild.build({
  entryPoints: ['src/admin.js', 'src/client.js'],
  outdir: 'dist',
  bundle: true,
  splitting: true,
  format: 'esm',
  entryNames: '[dir]/[name]-[hash]',
  chunkNames: 'chunks/[name]-[hash]'
});

Это не влияет на логику разделения, но определяет структуру выходной директории.

Особенности масштабирования

При увеличении числа entry points поведение системы становится нелинейным:

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

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

Поведение при пересборке

esbuild оптимизирован для инкрементальной сборки. При изменении одного entry point:

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

Однако при изменении shared модуля может затронуться несколько entry points одновременно, что увеличивает стоимость пересборки.

Итоговая модель работы нескольких entry points

Система разделения кода в esbuild строится на трёх принципах:

  • независимые графы входных точек
  • автоматическое выявление пересечений
  • контентно-зависимая генерация чанков

Результат определяется не декларативными правилами, а структурой импортов и реальными связями между модулями.