Node.js是单线程架构,所以通常如果想利用多核能力,需要借助多进程策略。
webpack在加载资源、构建实例、代码压缩等阶段都有开启多进程的处理办法。
其通用的思路是:
针对某种计算任务创建子进程,之后将运行所需参数通过 IPC 传递到子进程并启动计算操作,计算完毕后子进程再将结果通过 IPC 传递回主进程,寄宿在主进程的组件实例,再将结果提交给 Webpack
方案 | 描述 | 备注 |
---|---|---|
thread-loader | 加载过程多进程,官方方案 | 优点: 官方出品 |
缺点:在thread-loader 中运行的loader有限制 1. 不能获取 compilation、compiler 等实例和Webpack 配置; 2. 不能调用 emitAsset 等接口,这会导致 style-loader 这一类加载器无法正常工作; |
||
HappyPack | 加载过程多进程 | 优点: 大多时候能有效提升打包构建速度 |
缺点: 1. 不维护; 2. 可能会有兼容性问题; 3. 用法稍显复杂 |
||
parallel-webpack | 构造多个独立进程运行 Webpack 实例的方案 | |
terser-webpack-plugin | 代码压缩多进程,官方 |
threadLoader
这是webpack官方提出的解决方案。
Thread-loader 会在加载文件时创建新的进程,在子进程中使用 loader-runner 库运行 thread-loader 之后的 Loader 组件,执行完毕后再将结果回传到 Webpack 主进程,从而实现性能更佳的文件加载转译效果
先看下用法:
- 需要放在 use 数组首位
thread-loader还有一些配置:module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ["thread-loader", "babel-loader", "eslint-loader"],
},
],
},
};
- workers:子进程总数,默认值为 require(‘os’).cpus() - 1;
- workerParallelJobs:单个进程中并发执行的任务数;
- poolTimeout:子进程如果一直保持空闲状态,超过这个时间后会被关闭;
- poolRespawn:是否允许在子进程关闭后重新创建新的子进程,一般设置为 false 即可;
- workerNodeArgs:用于设置启动子进程时,额外附加的参数。
warmup 配置:为防止启动工作程序时出现高延迟,可以预热工作程序池。这将引导池中的最大工作程序数量,并将指定的模块加载到node.js模块高速缓存中。(类似线程池) ```javascript const threadLoader = require(“thread-loader”);module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: "thread-loader",
options: {
workers: 2, // 子进程总数
workerParallelJobs: 50, // 单个进程中并发执行的任务数
// ...
},
},
"babel-loader",
"eslint-loader",
],
},
],
},
};
threadLoader.warmup( { // 可传入上述 thread-loader 参数 workers: 2, workerParallelJobs: 50, }, [ // 子进程中需要预加载的 node 模块 “babel-loader”, “babel-preset-es2015”, “sass-loader”, ] );
threadLoader有两个突出的优点,一是官方团队出品,稳定性有保障;二是用法更简单。<br />但它也存在一些问题:
1. 在 Thread-loader 中运行的 Loader 不能调用 emitAsset 等接口,这会导致 style-loader 这一类加载器无法正常工作(解决方案是将这类组件放置在 thread-loader 之前,如 ['style-loader', 'thread-loader', 'css-loader'] );
2. Loader 中不能获取 compilation、compiler 等实例对象,也无法获取 Webpack 配置。
比如,配合babel-loader没啥问题:
```javascript
{
test: /\.jsx$/,
use: [
"thread-loader",
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-react"],
},
}
]
},
但是配合ts-loader就有点问题:
{
test: /\.tsx$/,
use: ["thread-loader", "ts-loader"],
},
happyPack
HappyPack :文件加载(Loader)操作拆散到多个子进程中并发执行,子进程执行完毕后再将结果合并回传到 Webpack 进程,从而提升构建性能。
其使用方法:
- 使用 happypack/loader 代替原本的 Loader 序列;
- 使用 HappyPack 插件注入代理执行 Loader 序列的逻辑
使用 happypack/loader :
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
use: "happypack/loader",
// 原始配置如:
// use: [
// {
// loader: 'babel-loader',
// options: {
// presets: ['@babel/preset-env']
// }
// },
// 'eslint-loader'
// ]
},
],
},
};
使用 happypack 插件实例new HappyPack():
plugins: [
new HappyPack({
// 将原本定义在 `module.rules.use` 中的 Loader 配置迁移到 HappyPack 实例中
loaders: [
{
loader: "babel-loader",
option: {
presets: ["@babel/preset-env"],
},
},
"eslint-loader",
],
}),
],
可以看到,原来的loader 配置被迁移到插件中了。
happyPack还有一种用法,就是针对不同类型的资源,分别应用多个happyPack实例,这种用法需要注意,加载资源的时候用参数id标记,另外需要happyPack实例中有此id相对应。
比如:happypack/loader?id=js,对应的id是js的happyPack实例: ```javascript const HappyPack = require(‘happypack’);
module.exports = {
// …
module: {
rules: [{
test: /.js?$/,
// 使用 id
参数标识该 Loader 对应的 HappyPack 插件示例
use: ‘happypack/loader?id=js’
},
{
test: /.less$/,
use: ‘happypack/loader?id=styles’
},
]
},
plugins: [
new HappyPack({
// 注意这里要明确提供 id 属性
id: ‘js’,
loaders: [‘babel-loader’, ‘eslint-loader’]
}),
new HappyPack({
id: ‘styles’,
loaders: [‘style-loader’, ‘css-loader’, ‘less-loader’]
})
]
};
> 多实例模式虽然能应对多种类型资源的加载需求,但默认情况下,HappyPack 插件实例 自行管理 自身所消费的进程,需要导致频繁创建、销毁进程实例 —— 这是非常昂贵的操作,反而会带来新的性能损耗。
针对这种频繁创建销毁问题的通用解法就是池,所以happyPack也有类似的进程池机制:HappyPack.ThreadPool
```javascript
const happyThreadPool = HappyPack.ThreadPool({
// 设置进程池大小
size: os.cpus().length - 1
});
声明进程池后,需要在每一个plugin实例中关联:
plugins: [
new HappyPack({
id: 'js',
// 设置共享进程池
threadPool: happyThreadPool,
loaders: ['babel-loader', 'eslint-loader']
}),
new HappyPack({
id: 'styles',
threadPool: happyThreadPool,
loaders: ['style-loader', 'css-loader', 'less-loader']
})
]
HappyPack 虽能有效提速,但它存在明显问题:
- 作者已不维护;
- 可能存在一些意想不到的兼容性问题(HappyPack 底层以自己的方式重新实现了加载器逻辑,源码与使用方法都不如 Thread-loader 清爽简单,而且会导致一些意想不到的兼容性问题,如 awesome-typescript-loader);
- 作用范围单一,性能收益有限。HappyPack 主要作用于文件加载阶段。
parallel-webpack
这是一种并行度更高,以多个独立进程运行 Webpack 实例的方案。(上面的threadLoader、happy Pack都是load资源时,文件加载过程的并行方案)
使用方法:
build时需要在package.json的script中替换原来的webpack为parallel-webpack。module.exports = [
{
entry: 'pageA.js',
output: {
path: './dist',
filename: 'pageA.js'
}
},
{
entry: 'pageB.js',
output: {
path: './dist',
filename: 'pageB.js'
}
}
];
更多用法参考:https://github.com/trivago/parallel-webpackterser-webpack-plugin
Webpack4 默认使用 Uglify-js 实现代码压缩,Webpack5 升级为 Terser,他们都提供多进程并行压缩的能力。
terser-webpack-plugin这样使用: ```javascript const TerserPlugin = require(“terser-webpack-plugin”);
module.exports = { optimization: { minimize: true, minimizer: [new TerserPlugin({ // 最大并行进程数为 2 parallel: 2 // number | boolean })], }, }; ```