Синхронные и асинхронные загрузчики

Модель выполнения загрузчиков

Загрузчики в Webpack представляют собой функции, которые преобразуют содержимое модулей перед тем, как они попадут в граф зависимостей и итоговый бандл. Каждый загрузчик выполняется в цепочке (pipeline), где результат одного становится входом для следующего.

По характеру выполнения загрузчики делятся на два типа:

  • синхронные
  • асинхронные

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

Webpack по умолчанию предполагает синхронное выполнение загрузчиков, если не указано обратное.


Синхронные загрузчики

Общий принцип

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

Сигнатура синхронного загрузчика:

module.exports = function (source) {
  return source;
};

Загрузчик получает на вход строку или Buffer, выполняет преобразование и сразу возвращает результат.


Пример простого синхронного загрузчика

module.exports = function (source) {
  const transformed = source.replaceAll('foo', 'bar');
  return transformed;
};

Такой загрузчик выполняется мгновенно и не блокирует поток управления Webpack.


Особенности синхронного выполнения

Синхронные загрузчики:

  • не поддерживают асинхронные операции (fs.readFile, fetch и т.п.)
  • не используют callback this.async()
  • возвращают результат через return
  • выполняются строго в порядке цепочки загрузчиков

Webpack обрабатывает их как чистые функции преобразования.


Обработка ошибок в синхронных загрузчиках

Ошибки выбрасываются через throw:

module.exports = function (source) {
  if (!source) {
    throw new Error('Пустой модуль');
  }

  return source;
};

Webpack перехватывает исключение и останавливает сборку для текущего модуля.


Когда применяются синхронные загрузчики

Синхронный режим используется в случаях, когда:

  • преобразование не требует I/O операций
  • работа ограничена строковыми или AST преобразованиями
  • важна предсказуемая и быстрая обработка

Примеры:

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

Асинхронные загрузчики

Общий принцип

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

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

Webpack предоставляет специальный механизм — this.async().


Использование callback-режима

Асинхронный загрузчик получает callback через this.async():

module.exports = function (source) {
  const callback = this.async();

  setTimeout(() => {
    const result = source.toUpperCase();
    callback(null, result);
  }, 100);
};

Сигнатура callback

callback(err, result, sourceMap, meta);

Параметры:

  • err — ошибка (или null)
  • result — преобразованный код
  • sourceMap — sourcemap (опционально)
  • meta — дополнительные данные

Асинхронная обработка через файловую систему

const fs = require('fs');

module.exports = function (source) {
  const callback = this.async();

  fs.readFile('./template.txt', 'utf-8', (err, template) => {
    if (err) return callback(err);

    const result = template.replace('{{content}}', source);
    callback(null, result);
  });
};

Promise-ориентированные загрузчики

Webpack поддерживает возврат Promise, что делает асинхронный код чище.

module.exports = function (source) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try {
        const result = source.trim();
        resolve(result);
      } catch (e) {
        reject(e);
      }
    }, 50);
  });
};

Если загрузчик возвращает Promise, Webpack автоматически ожидает его завершения.


async/await в загрузчиках

module.exports = async function (source) {
  const data = await Promise.resolve(source.toLowerCase());
  return data;
};

Асинхронная функция автоматически превращается в Promise-загрузчик.


Различия между синхронными и асинхронными загрузчиками

Модель выполнения

Синхронный:

  • выполняется мгновенно
  • возвращает результат через return
  • блокирует цепочку до завершения

Асинхронный:

  • может приостанавливать выполнение
  • использует callback, Promise или async/await
  • Webpack ожидает завершения перед переходом дальше

Поведение в цепочке загрузчиков

Цепочка загрузчиков выполняется справа налево, но тип выполнения влияет на управление потоком:

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

Совместимость в одной цепочке

Синхронные и асинхронные загрузчики могут комбинироваться:

module: {
  rules: [
    {
      test: /\.txt$/,
      use: [
        'sync-loader',
        'async-loader',
        'final-loader'
      ]
    }
  ]
}

В такой цепочке Webpack автоматически переключается между режимами исполнения в зависимости от загрузчика.


Контекст this в асинхронных загрузчиках

Объект this внутри загрузчика предоставляет API Webpack:

  • this.async()
  • this.callback()
  • this.cacheable()
  • this.emitFile()
  • this.resourcePath

При асинхронной работе особенно важен метод:

const callback = this.async();

Он переводит загрузчик в асинхронный режим. Если this.async() не вызван, Webpack считает загрузчик синхронным.


Асинхронные ошибки

В асинхронных загрузчиках ошибки передаются через callback или Promise:

Callback

callback(new Error('Ошибка обработки'), null);

Promise

return Promise.reject(new Error('Ошибка'));

Webpack корректно останавливает сборку текущего модуля и выводит ошибку в лог.


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

Асинхронные загрузчики не обязательно ускоряют сборку. Их цель — не блокировать поток выполнения при ожидании I/O.

Характер поведения:

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

Pitch-фаза и влияние на асинхронность

Загрузчики имеют две фазы:

  • normal (основная)
  • pitch (предварительная)

Асинхронность может использоваться и в pitch-фазе:

module.exports.pitch = function () {
  const callback = this.async();

  setTimeout(() => {
    callback(null, 'data from pitch');
  }, 10);
};

Pitch-фаза может остановить выполнение цепочки до перехода к normal-фазе других загрузчиков.


Контроль кеширования в асинхронных загрузчиках

Асинхронные операции часто влияют на кешируемость:

module.exports = function (source) {
  this.cacheable(true);

  return new Promise((resolve) => {
    setTimeout(() => resolve(source), 10);
  });
};

Webpack кеширует результат, если загрузчик объявлен как кешируемый и не зависит от внешнего состояния.


Практическая структура выбора типа загрузчика

Выбор между синхронным и асинхронным режимом определяется характером работы:

  • строковые преобразования → синхронный
  • AST-трансформации без I/O → синхронный
  • работа с файлами → асинхронный
  • взаимодействие с API → асинхронный
  • запуск внешних процессов → асинхронный

Комбинированные сценарии

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

module.exports = function (source) {
  if (source.includes('async')) {
    const callback = this.async();

    setTimeout(() => {
      callback(null, source);
    }, 20);

    return;
  }

  return source;
};

Такой подход позволяет адаптировать поведение под входные данные, сохраняя совместимость с Webpack pipeline.