Режим watch: детальное описание работы

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

Ключевая особенность заключается в том, что watch не запускает сборку «с нуля» при каждом изменении. Вместо этого используется сохранённое состояние графа модулей, что позволяет пересобирать только затронутые части проекта.


Запуск режима наблюдения (CLI и API)

Режим watch доступен в двух основных вариантах: через CLI и через программный API.

CLI-режим

При использовании CLI достаточно добавить флаг --watch:

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

В этом режиме esbuild:

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

CLI-режим прост, но ограничен в управлении поведением пересборки.


API-режим

API предоставляет более гибкий контроль:

import * as esbuild from 'esbuild';

await 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 получает результат каждой последующей сборки;
  • начальная сборка выполняется сразу;
  • дальнейшие изменения файлов запускают инкрементальные пересборки.

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

Esbuild не использует универсальный polling-цикл по умолчанию. Вместо этого применяется системный механизм наблюдения за файлами:

  • inotify на Linux;
  • FSEvents на macOS;
  • ReadDirectoryChangesW на Windows.

При невозможности использования нативных API возможен fallback к периодическому опросу.

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

Важно, что esbuild отслеживает:

  • исходные файлы;
  • подключаемые модули (import, require);
  • файлы, подключённые через плагины (onLoad).

Инкрементальные пересборки и граф зависимостей

В основе watch-режима лежит сохранение графа модулей между сборками.

После первой сборки esbuild сохраняет:

  • структуру модулей;
  • результаты разрешения путей (onResolve);
  • загруженный контент файлов (onLoad);
  • трансформации AST на внутреннем уровне.

При изменении файла происходит:

  1. фиксация события файловой системы;
  2. определение узла графа, связанного с файлом;
  3. инвалидирование только затронутых модулей;
  4. повторная сборка затронутой части графа.

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


Поведение при изменениях: добавление, удаление, переименование

Изменение содержимого файла

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


Добавление нового файла

Добавление нового файла не вызывает пересборку всего проекта автоматически. Однако:

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

Удаление файла

Удаление файла приводит к:

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

Переименование файла

Переименование обрабатывается как комбинация удаления и добавления. Это может привести к более широкой пересборке, если зависимости не удаётся сопоставить автоматически.


Обратные вызовы и обработка результатов сборки

В API режиме ключевым механизмом взаимодействия является onRebuild.

watch: {
  onRebuild(error, result) {
    // обработка результатов
  }
}

Поведение callback:

  • вызывается после каждой пересборки;
  • получает либо ошибку, либо объект результата;
  • не блокирует процесс наблюдения.

Объект result содержит:

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

Ошибки компиляции не останавливают watch-режим — он продолжает наблюдение за файлами.


Context API и современный watch-подход

Начиная с новых версий esbuild, предпочтение отдаётся Context API:

import * as esbuild from 'esbuild';

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

await ctx.watch();

Context API изменяет модель работы:

  • создаётся долгоживущий контекст сборки;
  • состояние графа сохраняется внутри контекста;
  • watch становится методом объекта, а не опцией конфигурации.

Также доступен ручной запуск пересборки:

await ctx.rebuild();

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

  • комбинировать watch с кастомной логикой;
  • управлять моментами пересборки;
  • интегрировать сборку в серверные процессы.

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

Watch-режим в esbuild оптимизирован под минимальное время отклика.

Основные оптимизации:

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

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

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

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


Ограничения режима watch

Несмотря на высокую скорость, режим наблюдения имеет архитектурные ограничения.

Отсутствие полноценного HMR

esbuild не предоставляет встроенного hot module replacement. При изменениях:

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

Ограниченная семантическая дифференциация

Система не анализирует изменения на уровне «функций» или «экспорта». Любое изменение файла приводит к инвалидированию модуля целиком.


Зависимость от файловой системы

В сложных средах (Docker, сетевые FS) наблюдение может работать менее стабильно из-за особенностей событий файловой системы.


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

Watch-режим часто используется совместно с сервером разработки.

Типичная схема:

  1. esbuild запускается в watch-режиме;
  2. при пересборке обновляется output;
  3. dev-сервер (Express, Vite-подобная логика, WebSocket сервер) отслеживает изменения;
  4. браузер получает уведомление и обновляет страницу.

Пример интеграции:

import * as esbuild from 'esbuild';
import http from 'http';

await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/bundle.js',
  watch: {
    onRebuild(err) {
      if (!err) {
        console.log('Обновление готово');
      }
    }
  }
});

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

В более сложных системах watch используется как источник событий для WebSocket-инвалидации страницы.


Поведение памяти и жизненный цикл watch

Watch-режим удерживает:

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

При долгом запуске это означает:

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

Остановка watch выполняется через завершение процесса или вызов ctx.dispose() в Context API.