Режим watch: пересборка при изменениях

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

Базовая модель работы watch

В основе watch-режима лежит постоянное отслеживание графа зависимостей проекта. При первой сборке esbuild:

  • строит граф импортов;
  • кэширует результаты трансформаций;
  • фиксирует набор входных файлов;
  • регистрирует файловые наблюдатели (file watchers).

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

Ключевая характеристика:

watch ≠ полная пересборка, watch = инкрементальный пересчёт графа зависимостей


Watch в CLI

В CLI-режиме watch активируется флагом --watch:

esbuild src/index.js --bundle --outfile=dist/bundle.js --watch

После запуска процесс не завершается. Вместо этого esbuild:

  • остаётся в активном состоянии;
  • следит за файловой системой;
  • пересобирает bundle при каждом изменении.

Типичное поведение при изменении файла:

[watch] build started
[watch] build finished in 42ms

Особенности CLI watch

  • нет встроенного API для обработки ошибок в коде;
  • логирование происходит в stdout;
  • процесс завершится только вручную (Ctrl+C);
  • оптимизирован под простые сценарии сборки фронтенда.

Watch в JavaScript API (legacy build)

В API esbuild watch включается через параметр watch в build:

import esbuild fr om "esbuild";

esbuild.build({
  entryPoints: ["src/index.js"],
  bundle: true,
  outfile: "dist/bundle.js",
  watch: {
    onRebuild(error, result) {
      if (error) {
        console.error("Ошибка пересборки:", error);
      } else {
        console.log("Пересборка завершена");
      }
    }
  }
});

Параметр onRebuild

onRebuild вызывается после каждого изменения файлов.

Сигнатура:

  • error — ошибка компиляции (если есть);
  • result — результат сборки при успешной пересборке.

Использование:

  • логирование ошибок;
  • интеграция с dev-сервером;
  • триггер reload в браузере;
  • синхронизация артефактов.

Контекстный API (Context) и современный watch

Современный подход esbuild основан на context, который заменяет классический build для долгоживущих процессов.

import * as esbuild from "esbuild";

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

await ctx.watch();

Преимущества context.watch

  • разделение этапов инициализации и запуска watch;
  • возможность управлять жизненным циклом сборки;
  • поддержка параллельных операций (serve + watch);
  • более чистая модель управления ресурсами.

Остановка watch-процесса

await ctx.dispose();

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


Инкрементальная пересборка и производительность

Главная особенность watch в esbuild — агрессивное использование инкрементальности.

При первом запуске:

  • анализируются все импорты;
  • формируется DAG зависимостей;
  • выполняются трансформации;
  • сохраняется внутренний кэш AST-подобных структур.

При изменении файла:

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

Это даёт:

  • пересборки в миллисекундном диапазоне;
  • стабильное время реакции независимо от размера проекта (в разумных пределах);
  • отсутствие «лавинообразного» роста времени пересборки.

Механизм отслеживания файлов

esbuild использует системные механизмы наблюдения файлов:

  • inotify (Linux);
  • FSEvents (macOS);
  • ReadDirectoryChangesW (Windows).

Особенности поведения

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

Ограничения файловых watcher’ов

Лимит дескрипторов

На Linux возможны ошибки вида:

ENOSPC: System lim it for number of file watchers reached

Решение — увеличение лимита inotify:

echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

Ограничения сетевых и виртуальных FS

Watch может работать нестабильно:

  • в Docker без корректных volume настроек;
  • в WSL при неправильной синхронизации FS;
  • на сетевых дисках.

Игнорирование файлов и оптимизация графа

esbuild автоматически исключает:

  • неиспользуемые зависимости;
  • модули вне цепочки импорта.

Но дополнительно можно уменьшать нагрузку через плагины:

plugins: [{
  name: "ignore-large",
  setup(build) {
    build.onResolve({ filter: /large-lib/ }, () => {
      return { path: "empty-module.js" };
    });
  }
}]

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


Поведение при ошибках компиляции

При ошибке:

  • первая сборка завершается с ошибкой;
  • watch остаётся активным;
  • последующие изменения автоматически триггерят новую попытку сборки.

Важно:

  • esbuild не завершает процесс при ошибке;
  • состояние ошибки не «залипает»;
  • после исправления кода сборка продолжается автоматически.

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

Типичный сценарий — связка с HTTP сервером:

import esbuild from "esbuild";
import http from "http";

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

await ctx.watch();

http.createServer((req, res) => {
  res.end("dev server running");
}).listen(3000);

В более сложных конфигурациях:

  • watch триггерит rebuild;
  • сервер отправляет HMR-события;
  • браузер обновляется через WebSocket.

Плагины и watch-совместимость

Плагины esbuild могут влиять на watch-поведение.

Важный аспект

Если плагин:

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

то необходимо корректно объявлять зависимости:

build.onLoad({ filter: /\.txt$/ }, async (args) => {
  const contents = await fs.promises.readFile(args.path, "utf8");

  return {
    contents,
    loader: "text"
  };
});

Если файл читается, но не зарегистрирован как зависимость, watch может не отследить изменения.


Частые причины отсутствия пересборки

Файл не входит в граф зависимостей

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

Неправильный путь импорта

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

  • ./utils.js
  • ../utils.js

Виртуальные модули без уведомления

При использовании onResolve и onLoad необходимо вручную обеспечивать триггер обновления.


Поведение при большом проекте

В крупных кодовых базах watch сохраняет стабильность благодаря:

  • дедупликации модулей;
  • кэшированию трансформаций;
  • ограничению повторного парсинга;
  • локализации изменений в графе.

Однако возможны узкие места:

  • большое число мелких файлов увеличивает нагрузку на FS watcher;
  • частые изменения одного модуля могут вызывать каскад пересборок;
  • плагины с тяжёлой логикой в onLoad могут замедлять rebuild.

Параллельный watch и multiple entry points

esbuild корректно обрабатывает несколько точек входа:

entryPoints: ["src/a.js", "src/b.js"]

Поведение:

  • строится общий граф зависимостей;
  • изменения в общем модуле пересобирают оба bundle;
  • независимые графы пересобираются изолированно.

Использование watch в монорепозиториях

В монорепозиториях важно учитывать:

  • пересечение зависимостей между пакетами;
  • внешние ссылки через symlinks;
  • необходимость корректного root dir.

Оптимизация:

  • явное указание absWorkingDir;
  • ограничение entryPoints по пакетам;
  • разделение context на независимые сборки.

Типичный жизненный цикл watch-процесса

  1. Инициализация контекста или build
  2. Построение графа зависимостей
  3. Первая сборка
  4. Активация файловых watchers
  5. Ожидание событий файловой системы
  6. Инкрементальная пересборка
  7. Обновление результата
  8. Повтор шагов 5–7 до завершения процесса