Компилятор и MultiCompiler

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

Компилятор Webpack не является «простой функцией сборки». Это сложная оркестрация этапов, включающая инициализацию конфигурации, создание окружения плагинов, построение графа модулей, оптимизацию, генерацию кода и эмиссию файлов.


Compiler как центральный объект Webpack

Роль Compiler

Объект Compiler является корневой сущностью всей системы Webpack. Он создаётся один раз при запуске сборки и далее управляет всем жизненным циклом процесса компиляции.

Основные функции Compiler:

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

Compiler можно рассматривать как «движок», который управляет всеми внутренними подсистемами Webpack.


Жизненный цикл Compiler

Компиляция проходит через строго определённые стадии, каждая из которых сопровождается хуками плагинов.

Инициализация

На этом этапе:

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

Ключевой момент: плагины получают доступ к compiler через метод apply.

class ExamplePlugin {
  apply(compiler) {
    compiler.hooks.initialize.tap('ExamplePlugin', () => {
      // логика инициализации
    });
  }
}

Запуск компиляции

Метод compiler.run() инициирует процесс однократной сборки.

Внутренне происходит:

  • создание объекта Compilation
  • запуск построения графа зависимостей
  • анализ entry points
  • последовательная обработка модулей

Watch-режим

При использовании compiler.watch() Webpack переходит в режим постоянного наблюдения за файловой системой.

Особенности:

  • используется файловый watcher (chokidar или нативные API)
  • изменения триггерят пересборку
  • сохраняется состояние предыдущей компиляции
  • используется кеширование модулей

Compilation как единица сборки

Назначение Compilation

Если Compiler — это глобальный менеджер процесса, то Compilation — это конкретный снимок одной сборки.

Каждый запуск run или watch создаёт новый экземпляр Compilation.

Его задачи:

  • построение dependency graph
  • обработка модулей
  • применение loaders
  • оптимизация чанков
  • генерация ассетов

Жизненный цикл Compilation

Compilation проходит следующие ключевые этапы:

  1. добавление entry modules
  2. построение графа зависимостей
  3. резолвинг модулей
  4. трансформация через loaders
  5. создание chunks
  6. оптимизация
  7. генерация assets

Каждый этап сопровождается хуками:

  • buildModule
  • seal
  • optimize
  • processAssets

Взаимодействие Compiler и Compilation

Связь между ними можно описать следующим образом:

  • Compiler управляет жизненным циклом
  • Compilation реализует конкретную сборку
  • Compiler может создавать множество Compilation последовательно
  • Compilation не существует без Compiler

В коде Webpack это выражается через создание нового объекта Compilation при каждом запуске:

compiler.hooks.compile.tap('Plugin', (params) => {
  const compilation = params.compilation;
});

Параллельная природа сборки

Webpack поддерживает асинхронность на уровне Compilation:

  • модули обрабатываются параллельно
  • loaders могут быть асинхронными
  • плагины могут встраивать async hooks

Это позволяет существенно ускорять сборку в больших проектах.


MultiCompiler: несколько компиляторов одновременно

Концепция MultiCompiler

MultiCompiler — это надстройка над стандартным Compiler, позволяющая запускать несколько независимых конфигураций в рамках одного процесса Node.js.

Он создаётся при передаче массива конфигураций:

module.exports = [
  configA,
  configB,
  configC
];

Webpack в этом случае не создаёт один Compiler, а формирует MultiCompiler, который агрегирует несколько независимых Compiler-инстансов.


Архитектура MultiCompiler

MultiCompiler можно представить как контейнер:

  • содержит массив Compiler
  • делегирует вызовы каждому Compiler
  • агрегирует результаты выполнения
  • синхронизирует watch-режим

Ключевая особенность: каждый Compiler остаётся полностью изолированным.


Поведение MultiCompiler при запуске

Запуск build

При вызове multiCompiler.run() происходит:

  • последовательный или параллельный запуск всех Compiler
  • каждый Compiler создаёт собственный Compilation
  • результаты объединяются в общий поток вывода

Watch режим

В режиме наблюдения:

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

Изоляция конфигураций

Каждый Compiler внутри MultiCompiler:

  • имеет собственный dependency graph
  • использует собственные loaders
  • применяет свои плагины
  • генерирует отдельные output assets

Это исключает перекрёстное влияние конфигураций.


Ограничения MultiCompiler

Несмотря на удобство, MultiCompiler имеет ряд ограничений:

1. Отсутствие общего кеша

Каждый Compiler работает со своим кешем, что увеличивает потребление памяти.

2. Дублирование ресурсов

Одинаковые зависимости в разных конфигурациях обрабатываются независимо.

3. Сложность отладки

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


Сценарии использования MultiCompiler

Сборка разных окружений

Типичный случай:

  • клиентская сборка
  • серверная сборка (SSR)
  • тестовая сборка

Каждая конфигурация изолирована, но запускается одновременно.


Мультиформатная сборка

Примеры:

  • ESM + CommonJS
  • development + production
  • legacy + modern bundles

Монорепозитории

MultiCompiler часто используется для:

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

Внутреннее управление жизненным циклом MultiCompiler

MultiCompiler не реализует собственный lifecycle hooks. Вместо этого он:

  • проксирует хуки каждого Compiler
  • объединяет события в общий поток
  • синхронизирует watch-инстансы

Каждый Compiler остаётся самостоятельной единицей, что упрощает внутреннюю архитектуру Webpack.


Событийная модель в MultiCompiler

При использовании MultiCompiler события выглядят как:

  • compiler A emit
  • compiler B emit
  • compiler C emit

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

Это создаёт иллюзию единой сборки при фактически независимых процессах.


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

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

  • количества конфигураций
  • размера dependency graph каждого Compiler
  • уровня параллелизма loaders
  • использования кеширования

В типичной ситуации MultiCompiler масштабируется линейно по числу конфигураций, что важно учитывать при архитектуре сборки.


Взаимодействие плагинов с MultiCompiler

Плагины в MultiCompiler:

  • применяются отдельно к каждому Compiler
  • не разделяются между конфигурациями
  • не имеют глобального состояния по умолчанию

Однако возможно создание плагинов, которые специально работают с MultiCompiler через проверку типа инстанса.


Итоговая модель взаимодействия

Архитектурно система выглядит так:

  • MultiCompiler (опциональный уровень)

    • Compiler (уровень конфигурации)

      • Compilation (уровень конкретной сборки)

        • Modules
        • Loaders
        • Assets

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