Опция incremental (устаревшее API, контекст)

incremental режим в esbuild относится к устаревшему API, которое использовалось для ускорения повторных сборок за счёт сохранения внутреннего состояния графа модулей между вызовами. Его ключевая идея — избегать полной пересборки проекта при каждом изменении, ограничиваясь пересчётом только затронутых файлов.

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

Основной результат первой сборки содержит дополнительный объект, который позволяет инициировать повторную сборку без повторного анализа всей файловой структуры.

import * as esbuild from 'esbuild';

const result = await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/bundle.js',
  incremental: true
});

// повторная сборка с учётом изменений
await result.rebuild();

Метод rebuild() использует ранее построенный граф и пересчитывает только изменившиеся модули. Это существенно ускоряет цикл разработки в проектах с большим количеством файлов.

Структура результата incremental-сборки

При включении incremental: true результат build() расширяется дополнительными возможностями:

  • rebuild() — повторная сборка на основе сохранённого состояния
  • dispose() — освобождение памяти, занятой графом модулей
await result.dispose();

dispose() критически важен, поскольку incremental-контекст удерживает значительный объём памяти. При длительной работе без очистки возможно накопление утечек памяти, особенно в процессах с частыми пересборками или в тестовых средах.

Особенности внутреннего кэша

Incremental режим опирается на несколько уровней кэширования:

  • кэш разрешения путей (module resolution cache)
  • кэш AST после парсинга
  • кэш трансформаций плагинов
  • кэш графа зависимостей

При вызове rebuild() esbuild старается переиспользовать эти структуры, пересчитывая только изменённые узлы графа. Если изменения затрагивают входные точки или конфигурацию плагинов, часть кэша инвалидируется.

Ограничения incremental API

Incremental режим имеет ряд ограничений:

  • нельзя изменять конфигурацию сборки между rebuild() без создания нового контекста
  • изменения плагинов не всегда корректно отражаются в кэше
  • ограниченная гибкость при динамическом изменении entryPoints
  • устаревший статус API и отсутствие новых оптимизаций

Особенно важно ограничение на конфигурацию: любые изменения опций, влияющих на граф сборки (например, define, jsxFactory, platform), требуют новой полной сборки.

Причины устаревания incremental

Incremental API был промежуточным решением до появления более универсального механизма — контекста сборки. Основная проблема incremental заключалась в том, что он был «надстройкой» над обычным build(), а не полноценной абстракцией управления жизненным циклом сборки.

Это приводило к:

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

Переход к context API

Современная альтернатива incremental — context API, который предоставляет более структурированное управление сборкой.

Эквивалент incremental через context:

import * as esbuild from 'esbuild';

const ctx = await esbuild.context({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/bundle.js'
});

// первая сборка
await ctx.rebuild();

// повторные сборки
await ctx.rebuild();

await ctx.dispose();

В отличие от incremental, context API:

  • явно создаёт и управляет жизненным циклом сборки
  • отделяет конфигурацию от выполнения
  • предоставляет расширенные возможности watch-режима
  • лучше интегрируется с долгоживущими процессами (серверы, dev tools)

Сравнение incremental и context

Incremental:

  • возвращается из build()
  • хранит состояние внутри результата
  • ограниченная модификация конфигурации
  • устаревшая модель управления

Context:

  • создаётся отдельно через esbuild.context()
  • поддерживает явное управление жизненным циклом
  • лучше масштабируется
  • является актуальным стандартом API

Поведение при изменениях файлов

И incremental, и context используют одинаковую стратегию отслеживания изменений файловой системы. При изменении исходного файла:

  1. фиксируется изменение по timestamp или watcher-событию
  2. инвалидируется соответствующий узел графа
  3. пересобирается только затронутая подграфовая часть
  4. итоговый бандл обновляется

Разница заключается в том, как хранится и управляется этот граф.

Интеграция с watch-режимом

Incremental часто использовался как основа для ручной реализации watch:

import * as esbuild from 'esbuild';

const result = await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/bundle.js',
  incremental: true
});

require('fs').watch('src', async () => {
  await result.rebuild();
});

Однако такой подход был низкоуровневым и требовал самостоятельного управления debounce, обработкой ошибок и очисткой ресурсов.

Context API решает это более системно через встроенный watch режим:

const ctx = await esbuild.context({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/bundle.js'
});

await ctx.watch();

Память и жизненный цикл

Incremental-контекст удерживает:

  • AST всех модулей
  • граф зависимостей
  • кэши плагинов
  • внутренние структуры резолвинга

При частых rebuild без dispose() память может расти линейно. Это особенно критично в CI-средах и долгоживущих dev-серверах.

Контекстный API делает управление ресурсами более предсказуемым: жизненный цикл явно завершается вызовом dispose(), освобождая все внутренние структуры.

Практическое значение legacy-режима

Incremental API сохраняет значение только в поддержке старых кодовых баз и библиотек, где переход на context требует изменения архитектуры инструментария. В новых проектах он не используется, так как context полностью перекрывает его функциональность и расширяет её.

Миграция обычно сводится к замене:

  • esbuild.build({ incremental: true }) на
  • esbuild.context() + rebuild()

с последующим добавлением явного управления жизненным циклом.