Webpack способен собирать проект сразу под несколько окружений и платформ:
Подобная архитектура называется multi-target build или multi-compiler mode. Вместо запуска нескольких отдельных команд используется единая конфигурация, возвращающая массив настроек.
Параллельная сборка особенно важна в крупных приложениях:
Webpack поддерживает экспорт массива конфигураций:
// webpack.config.js
const path = require('path');
module.exports = [
{
name: 'client',
target: 'web',
entry: './src/client.js',
output: {
filename: 'client.bundle.js',
path: path.resolve(__dirname, 'dist/client')
}
},
{
name: 'server',
target: 'node',
entry: './src/server.js',
output: {
filename: 'server.bundle.js',
path: path.resolve(__dirname, 'dist/server')
}
}
];
Webpack создаёт два независимых компилятора:
Каждый компилятор имеет:
При экспорте массива Webpack создаёт объект
MultiCompiler.
Внутри:
MultiCompiler
├── Compiler(client)
└── Compiler(server)
Каждый compiler запускается параллельно, если это возможно.
Это позволяет:
Webpack сам не создаёт полноценные worker-пулы для компиляции, однако способен:
Пример:
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: true
})
]
}
};
В этом случае минификация распределяется между CPU cores.
Одна из самых распространённых схем.
project/
├── src/
│ ├── client/
│ └── server/
├── dist/
└── webpack.config.js
const path = require('path');
const clientConfig = {
name: 'client',
target: 'web',
entry: './src/client/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist/client')
}
};
const serverConfig = {
name: 'server',
target: 'node',
entry: './src/server/index.js',
output: {
filename: 'server.js',
path: path.resolve(__dirname, 'dist/server')
}
};
module.exports = [clientConfig, serverConfig];
Webpack поддерживает множество таргетов.
| Target | Назначение |
|---|---|
| web | браузер |
| node | Node.js |
| webworker | Web Worker |
| electron-main | Electron main process |
| electron-renderer | Electron renderer |
| async-node | асинхронный Node runtime |
module.exports = [
{
name: 'app',
target: 'web',
entry: './src/app.js',
output: {
filename: 'app.js'
}
},
{
name: 'worker',
target: 'webworker',
entry: './src/worker.js',
output: {
filename: 'worker.js'
}
}
];
В больших проектах массив конфигураций быстро разрастается.
Типичная структура:
webpack/
├── webpack.client.js
├── webpack.server.js
├── webpack.worker.js
└── webpack.common.js
module.exports = [
require('./webpack/webpack.client'),
require('./webpack/webpack.server'),
require('./webpack/webpack.worker')
];
Подход повышает:
Общие настройки выносятся отдельно.
module.exports = {
resolve: {
extensions: ['.js', '.json']
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
}
};
const common = require('./webpack.common');
module.exports = {
...common,
target: 'web',
entry: './src/index.js'
};
При сложной архитектуре spread становится неудобным.
Используется пакет:
npm install webpack-merge --save-dev
const { merge } = require('webpack-merge');
const common = require('./webpack.common');
module.exports = merge(common, {
target: 'web',
mode: 'production',
entry: './src/index.js'
});
Frontend и backend часто требуют разных режимов.
module.exports = [
{
name: 'client',
mode: 'production',
target: 'web'
},
{
name: 'server',
mode: 'development',
target: 'node'
}
];
Например:
Webpack позволяет указывать зависимости через
dependencies.
module.exports = [
{
name: 'client',
entry: './src/client.js'
},
{
name: 'server',
dependencies: ['client'],
entry: './src/server.js'
}
];
Сначала завершится client compiler, затем server compiler.
Типичные сценарии:
Webpack 5 поддерживает filesystem cache.
module.exports = [
{
name: 'client',
cache: {
type: 'filesystem'
}
},
{
name: 'server',
cache: {
type: 'filesystem'
}
}
];
Без разделения кэш может конфликтовать.
Правильный вариант:
cache: {
type: 'filesystem',
cacheDirectory: path.resolve(
__dirname,
'.cache/client'
)
}
Для server:
cache: {
type: 'filesystem',
cacheDirectory: path.resolve(
__dirname,
'.cache/server'
)
}
Библиотеки часто публикуются в нескольких форматах:
const path = require('path');
module.exports = [
{
name: 'cjs',
target: 'node',
entry: './src/index.js',
output: {
filename: 'index.cjs.js',
libraryTarget: 'commonjs2',
path: path.resolve(__dirname, 'dist')
}
},
{
name: 'esm',
target: 'web',
entry: './src/index.js',
experiments: {
outputModule: true
},
output: {
filename: 'index.esm.js',
module: true,
path: path.resolve(__dirname, 'dist')
}
}
];
Каждый target должен содержать только необходимые loaders.
Плохой вариант:
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
}
для server bundle.
if (target === 'web') {
rules.push({
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
});
}
Для тяжёлых Babel/TypeScript трансформаций применяется
thread-loader.
npm install thread-loader --save-dev
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
'thread-loader',
{
loader: 'babel-loader'
}
]
}
]
}
};
Потоки не всегда ускоряют сборку.
Проблемы:
Максимальный эффект достигается:
Разные таргеты могут иметь разные minimizer-настройки.
const TerserPlugin = require('terser-webpack-plugin');
module.exports = [
{
name: 'client',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true
})
]
}
},
{
name: 'server',
optimization: {
minimize: false
}
}
];
Backend-сборки обычно не включают node_modules внутрь bundle.
npm install webpack-node-externals --save-dev
const nodeExternals = require('webpack-node-externals');
module.exports = {
target: 'node',
externals: [nodeExternals()]
};
Это:
Webpack способен отслеживать изменения сразу для всех конфигураций.
webpack --watch
Все compiler instances переходят в watch mode одновременно.
При изменении файлов:
Для multi-target сборок обычно dev-server применяется только для frontend.
module.exports = [
{
name: 'client',
devServer: {
hot: true,
port: 3000
}
},
{
name: 'server',
target: 'node'
}
];
Одна из критических ошибок — одинаковые output paths.
Плохой пример:
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
для нескольких конфигураций одновременно.
output: {
path: path.resolve(__dirname, 'dist/client')
}
и:
output: {
path: path.resolve(__dirname, 'dist/server')
}
Поле name крайне важно.
{
name: 'client'
}
Webpack использует его:
Webpack 5 поддерживает расширенное логирование.
module.exports = {
infrastructureLogging: {
level: 'verbose'
}
};
Для multi-target проектов это особенно полезно.
Можно получать отдельную статистику для каждого compiler.
stats: {
preset: 'normal'
}
или:
stats: 'errors-warnings'
Крупный SSR-проект может содержать:
configs/
├── client
├── server
├── worker
├── admin
├── mobile
└── legacy
Webpack одновременно собирает:
Иногда конфигурации генерируются автоматически.
const targets = [
'web',
'node',
'webworker'
];
module.exports = targets.map(target => ({
name: target,
target,
entry: `./src/${target}.js`,
output: {
filename: `${target}.bundle.js`
}
}));
module.exports = env => {
const configs = [];
if (env.client) {
configs.push(clientConfig);
}
if (env.server) {
configs.push(serverConfig);
}
return configs;
};
webpack --config-name client
или:
webpack --config-name server
Это особенно удобно:
Существуют сторонние инструменты для агрессивной параллелизации.
Например:
npm install parallel-webpack --save-dev
Однако в Webpack 5 встроенный multi-compiler часто оказывается достаточным.
Каждый compiler создаёт:
На крупных проектах RAM usage может быть очень высоким.
Несколько compiler instances:
Появляются проблемы:
Наиболее устойчивая схема:
webpack/
├── common/
├── client/
├── server/
├── shared/
├── plugins/
└── utils/
Каждый target обязан иметь:
Нельзя:
Для каждого target:
.cache/client
.cache/server
.cache/admin
Frontend и backend требуют разных стратегий:
| Frontend | Backend |
|---|---|
| aggressive splitting | минимальная обработка |
| tree shaking | readable output |
| minimization | быстрый rebuild |
| asset optimization | externals |
client build
├── js bundles
├── css assets
└── manifest.json
server build
├── renderer.js
└── SSR templates
Server compiler использует assets client compiler через dependencies и manifest-файлы.
Module Federation также может использовать multi-compiler.
Например:
host
remote-admin
remote-shop
remote-profile
Каждый remote способен собираться отдельным compiler instance.
Webpack поддерживает profiling.
webpack --profile --json > stats.json
Далее анализ выполняется через:
Подход особенно эффективен при:
Иногда независимые build pipelines оказываются проще.
Особенно если:
В таких случаях отдельные webpack-конфигурации и независимые процессы могут быть стабильнее и проще в сопровождении.