esbuild изначально проектировался как сверхбыстрый сборщик, ориентированный на минимизацию накладных расходов во время сборки. Архитектура инструмента строится вокруг строгой модели выполнения: большая часть этапов компиляции и трансформации предполагает синхронное поведение, что позволяет эффективно распараллеливать работу на уровне Go-рантайма.
Плагины в этой системе являются точками расширения, но их выполнение подчинено тем же ограничениям. Внутри ядра esbuild нет полноценного event loop, аналогичного Node.js, поэтому асинхронность реализуется через ограниченные механизмы: возврат промисов в хуках и ожидание их завершения до продолжения пайплайна.
Ключевое ограничение заключается в том, что асинхронные операции допустимы только в рамках определённых стадий плагинного API. Любая попытка внедрить произвольный неблокирующий код вне этих стадий приводит к невозможности корректного завершения сборки.
Асинхронные плагины в esbuild строятся вокруг функций, которые могут
возвращать Promise. Основной принцип: если хук возвращает
промис, сборщик автоматически приостанавливает выполнение до его
разрешения.
Типовой плагин определяется через функцию setup, внутри
которой регистрируются обработчики событий:
onResolveonLoadonStartonEndКаждый из этих хуков может быть асинхронным, но наиболее часто
асинхронность используется в onResolve и
onLoad, так как именно они участвуют в разрешении модулей и
загрузке содержимого файлов.
Асинхронность реализуется не через отдельный поток выполнения, а через ожидание завершения промисов внутри общего пайплайна сборки.
onResolve отвечает за преобразование импортов в
конкретные пути или виртуальные модули. Асинхронная версия этого хука
позволяет выполнять операции, зависящие от внешних источников: файловой
системы, сетевых запросов, баз данных метаданных.
Пример структуры:
const asyncResolvePlugin = {
name: 'async-resolve',
setup(build) {
build.onResolve({ filter: /^virtual:/ }, async (args) => {
const data = await fetchMetadata(args.path);
return {
path: data.realPath,
namespace: 'virtual-ns'
};
});
}
};
В этом примере важно, что возвращается Promise, а не
объект напрямую. Esbuild приостанавливает процесс резолва до получения
результата.
Особенность заключается в том, что асинхронный onResolve
может существенно влиять на производительность всей сборки. Даже один
медленный промис увеличивает latency всей цепочки модулей, поскольку
резолвинг имеет древовидную структуру зависимостей.
onLoad используется для получения содержимого модуля
после его резолва. Это один из самых частых сценариев применения
асинхронности, особенно при интеграции с удалёнными источниками или
динамической генерацией кода.
const asyncLoadPlugin = {
name: 'async-load',
setup(build) {
build.onLoad({ filter: /\.txt$/ }, async (args) => {
const content = await readRemoteFile(args.path);
return {
contents: content,
loader: 'text'
};
});
}
};
Здесь асинхронная операция может включать:
Важно, что результат должен быть приведён к структуре, ожидаемой
esbuild: contents и loader. Любые задержки в
этом этапе напрямую увеличивают время построения графа модулей.
Асинхронные плагины влияют на построение dependency graph, поскольку
каждый await в onResolve или
onLoad может задерживать раскрытие ветвей графа.
Граф модулей в esbuild строится лениво: модуль не загружается, пока не завершено разрешение его зависимостей. Это означает, что асинхронность может привести к каскадным задержкам.
Характерная проблема:
onResolve делает запрос к внешнему APIДля минимизации эффекта применяются техники:
Несмотря на синхронную модель ядра, асинхронные плагины могут выполняться параллельно на уровне промисов. Esbuild запускает несколько хуков одновременно, но синхронизирует результат перед переходом к следующей стадии.
Важно различать:
Например, несколько onLoad могут выполняться
одновременно, если они не зависят друг от друга. Однако переход к
следующему этапу невозможен до завершения всех активных промисов
текущего этапа.
Ошибки в промисах обрабатываются стандартным механизмом отклонения
(Promise.reject). Esbuild перехватывает такие ошибки и
прерывает сборку.
build.onLoad({ filter: /\.data$/ }, async (args) => {
const data = await loadData(args.path);
if (!data) {
throw new Error(`Не удалось загрузить ${args.path}`);
}
return {
contents: JSON.stringify(data),
loader: 'json'
};
});
Поведение при ошибке:
Особенность заключается в том, что асинхронные ошибки не всегда сохраняют полный контекст источника, поэтому важна явная диагностика на уровне плагина.
Несмотря на гибкость, асинхронные плагины в esbuild имеют ряд ограничений:
Асинхронность существует только как механизм ожидания результата, но не как полноценная модель конкурентного исполнения.
Кэширование является ключевым инструментом оптимизации, так как
каждый await в цепочке влияет на общую
производительность.
Типовые стратегии:
Локальный in-memory cache
const cache = new Map();
build.onLoad({ filter: /\.api$/ }, async (args) => {
if (cache.has(args.path)) {
return cache.get(args.path);
}
const result = await fetchAPI(args.path);
const output = {
contents: result,
loader: 'json'
};
cache.set(args.path, output);
return output;
});
Дедупликация запросов
Если несколько модулей запрашивают один и тот же ресурс, используется shared promise:
const inflight = new Map();
function getData(path) {
if (inflight.has(path)) return inflight.get(path);
const promise = fetchData(path).finally(() => {
inflight.delete(path);
});
inflight.set(path, promise);
return promise;
}
Асинхронность особенно полезна при создании виртуальных модулей, где содержимое генерируется динамически:
Такие модули часто не существуют в файловой системе, и их содержимое формируется на основе внешних источников.
build.onResolve({ filter: /^config:/ }, (args) => {
return {
path: args.path,
namespace: 'config-ns'
};
});
build.onLoad({ filter: /.*/, namespace: 'config-ns' }, async (args) => {
const config = await loadConfig();
return {
contents: JSON.stringify(config),
loader: 'json'
};
});
В реальных сборочных системах асинхронные плагины применяются ограниченно и только в некритичных частях пайплайна. Основные рекомендации:
onResolveonLoadАсинхронность в контексте esbuild рассматривается как инструмент интеграции, а не как основной механизм вычислений.
Асинхронные плагины часто используются для интеграции с Node.js API:
Однако важно учитывать, что esbuild не эмулирует полноценную Node.js среду. Любая зависимость от специфичных runtime-особенностей может привести к несовместимости при сборке в разных окружениях.
Асинхронность здесь выступает связующим слоем между ограниченным API сборщика и внешними системами.