В реальных проектах редко существует единственная среда выполнения. Один и тот же код может предназначаться сразу для нескольких окружений: браузер, Node.js, edge-runtime, тестовые стенды, различные версии ECMAScript или даже разные форматы модулей (ESM и CommonJS). Это приводит к необходимости формировать несколько независимых сборок из одного исходного кода.
Под многотаргетной сборкой в контексте Esbuild понимается процесс,
при котором один исходный проект компилируется в несколько выходных
конфигураций с различными параметрами: платформой
(platform), целевым стандартом (target),
форматом модулей (format), точками входа и настройками
бандлинга.
Ключевая особенность Esbuild заключается в том, что такие сборки могут выполняться параллельно, поскольку API инструмента изначально асинхронный и оптимизирован под конкурентное выполнение задач.
Esbuild реализован на Go и использует модель конкурентного выполнения, в которой каждая задача сборки может выполняться независимо. JavaScript-обёртка над Esbuild лишь отправляет запросы в нативный процесс, который способен обрабатывать несколько задач одновременно.
Основные характеристики:
buildЭта архитектура делает параллельную сборку не просто возможной, а естественной формой использования инструмента.
Самый простой способ запустить несколько таргетов — использовать
Promise.all, где каждая сборка представляет собой отдельный
вызов esbuild.build.
import * as esbuild fr om 'esbuild';
await Promise.all([
esbuild.build({
entryPoints: ['src/index.ts'],
outfile: 'dist/node.cjs',
bundle: true,
platform: 'node',
format: 'cjs',
target: 'node18'
}),
esbuild.build({
entryPoints: ['src/index.ts'],
outfile: 'dist/browser.js',
bundle: true,
platform: 'browser',
format: 'esm',
target: 'es2020'
})
]);
В этом примере обе сборки запускаются одновременно, и каждая использует собственный граф зависимостей и настройки трансформации.
Важно, что Esbuild не разделяет кеш между независимыми вызовами
build, если не используется context API,
поэтому каждая сборка строит граф отдельно.
В многотаргетных проектах обычно выделяется базовая конфигурация и набор специфичных параметров для каждого окружения.
const baseConfig = {
entryPoints: ['src/index.ts'],
bundle: true,
sourcemap: true,
minify: true
};
const targets = [
{
...baseConfig,
outfile: 'dist/app.node.js',
platform: 'node',
format: 'cjs',
target: 'node20'
},
{
...baseConfig,
outfile: 'dist/app.browser.js',
platform: 'browser',
format: 'esm',
target: 'es2022'
},
{
...baseConfig,
outfile: 'dist/app.legacy.js',
platform: 'browser',
format: 'iife',
target: 'es5'
}
];
await Promise.all(targets.map(config => esbuild.build(config)));
Такой подход уменьшает дублирование и позволяет масштабировать количество таргетов без усложнения логики.
Несмотря на высокую производительность Esbuild, параллельный запуск множества тяжёлых сборок может привести к перегрузке CPU и памяти. Особенно это проявляется при:
minifyДля контроля нагрузки используется ограничение конкурентности.
async function buildInBatches(tasks, batchSize) {
for (let i = 0; i < tasks.length; i += batchSize) {
const batch = tasks.slice(i, i + batchSize);
await Promise.all(batch.map(fn => fn()));
}
}
import pLimit fr om 'p-lim it';
import * as esbuild fr om 'esbuild';
const lim it = pLimit(2);
const builds = targets.map(config =>
lim it(() => esbuild.build(config))
);
await Promise.all(builds);
Ограничение параллелизма особенно важно в CI/CD средах, где ресурсы фиксированы.
Каждый вызов esbuild.build строит собственный граф
модулей. Это означает, что при нескольких таргетах один и тот же файл
может парситься несколько раз.
Для оптимизации используется API context, который
позволяет повторно использовать внутренние структуры.
const ctx = await esbuild.context({
entryPoints: ['src/index.ts'],
bundle: true
});
await Promise.all([
ctx.rebuild({
outfile: 'dist/node.js',
platform: 'node'
}),
ctx.rebuild({
outfile: 'dist/browser.js',
platform: 'browser'
})
]);
Хотя context чаще используется для watch-режима, он
также уменьшает накладные расходы при повторных сборках.
При параллельной сборке разных таргетов важно учитывать различия в рантаймах.
fs, path,
processЭти различия напрямую влияют на конфигурацию Esbuild и требуют изоляции сборок.
В монорепозиториях параллельная сборка приобретает дополнительную сложность из-за множества пакетов и взаимных зависимостей.
Типичная структура:
packages/uipackages/corepackages/serverpackages/utilsКаждый пакет может иметь несколько таргетов:
const packages = ['ui', 'core', 'server', 'utils'];
const builds = packages.flatMap(pkg => [
esbuild.build({
entryPoints: [`packages/${pkg}/src/index.ts`],
outfile: `dist/${pkg}/node.js`,
platform: 'node'
}),
esbuild.build({
entryPoints: [`packages/${pkg}/src/index.ts`],
outfile: `dist/${pkg}/browser.js`,
platform: 'browser'
})
]);
await Promise.all(builds);
Такой подход приводит к квадратичному росту количества сборок, что делает контроль параллелизма критически важным.
При параллельной сборке нескольких таргетов необходимо исключать конфликты файловой системы. Esbuild не управляет блокировками файлов, поэтому ответственность за корректные пути лежит на конфигурации.
Практика:
dist/node,
dist/browser)Плагины Esbuild могут нарушать корректность параллельной сборки, если используют общие переменные или файловое состояние.
Типичные ошибки:
Корректный подход — делать плагины чистыми функциями:
const plugin = () => ({
name: 'example',
setup(build) {
const cache = new Map();
build.onResolve({ filter: /.*/ }, args => {
if (cache.has(args.path)) return cache.get(args.path);
const result = { path: args.path };
cache.set(args.path, result);
return result;
});
}
});
Каждый вызов build получает собственный экземпляр
плагина, что обеспечивает изоляцию.
Минификация — одна из самых ресурсоёмких операций в Esbuild. При параллельной сборке нескольких таргетов она становится главным фактором нагрузки.
Особенности:
Для снижения нагрузки часто используют стратегию разделения:
При параллельной сборке нескольких таргетов часто одновременно формируются разные типы сборок:
await Promise.all([
esbuild.build({
entryPoints: ['src/index.ts'],
outfile: 'dist/dev.js',
minify: false,
sourcemap: true
}),
esbuild.build({
entryPoints: ['src/index.ts'],
outfile: 'dist/prod.js',
minify: true,
sourcemap: false
})
]);
Такой подход позволяет получать несколько артефактов за один цикл выполнения без повторного запуска процесса.
outfile для разных
таргетовformat при одинаковом output
имениКаждая из этих ошибок приводит к нестабильным или некорректным результатам сборки.
При росте проекта параллельная стратегия требует структурирования:
В крупных системах Esbuild часто используется как быстрый слой трансформации, а оркестрация параллельных таргетов выносится в отдельный build-tool слой.