多进程打包
受限于 Node.js 的单线程架构,原生 Webpack 对所有资源文件做的所有解析、转译、合并操作本质上都是在同一个线程内串行执行,CPU 利用率极低,因此,理所当然地社区出现了一些基于多进程方式运行 Webpack,或 Webpack 构建过程某部分工作的方案,例如:
- HappyPack:多进程方式运行资源加载逻辑
- Thread-loader:Webpack 官方出品,同样以多进程方式运行资源加载逻辑
- TerserWebpackPlugin:支持多进程方式执行代码压缩、uglify 功能
- Parallel-Webpack:多进程方式运行多个 Webpack 构建实例
这些方案的核心设计都很类似:针对某种计算任务创建子进程,之后将运行所需参数通过 IPC 传递到子进程并启动计算操作,计算完毕后子进程再将结果通过 IPC 传递回主进程,寄宿在主进程的组件实例再将结果提交给 Webpack。
1. 使用 Thread-loader
Thread-loader 功能上与 HappyPack 极为相近,两者主要区别:
- Thread-loader 由 Webpack 官方提供,目前还处于持续迭代维护状态,理论上更可靠
- Thread-loader 只提供了一个单一的 loader 组件,用法上相对更简单
- HappyPack 启动后,会向其运行的 loader 注入
emitFile
等接口,而 Thread-loader 则不具备这一特性,因此对 loader 的要求会更高,兼容性较差
1) 使用方法
首先,需要安装 Thread-loader 依赖:
yarn add -D thread-loader
其次,需要将 Thread-loader 配置到 loader 数组首位,确保最先运行,如:
module.exports = {
module: {
rules: [
{
test: /\.tsx?$/,
use: [
'thread-loader',
'babel-loader',
'ts-loader'
],
},
],
},
};
以 Three.js 为例,开启 Thread-loader 前,构建耗时大约为 11000ms 到 18000ms 之间,开启后耗时降低到 8000ms 左右,提升约37%。
2) 原理
Webpack 将执行 Loader 相关逻辑都抽象到 loader-runner
库,Thread-loader 也同样复用该库完成 Loader 的运行逻辑,核心步骤:
- 启动时,以
pitch
方式拦截 Loader 执行链 - 分析 Webpack 配置对象,获取
thread-loader
后面的 Loader 列表 - 调用
child_process.spawn
创建工作子进程,并将Loader 列表、文件路径、上下文等参数传递到子进程 - 子进程中调用
loader-runner
,转译文件内容 - 转译完毕后,将结果传回主进程
3) 缺点
Thread-loader 是 Webpack 官方推荐的并行处理组件,实现与使用都非常简单,但它也存在一些问题:
- Loader 中不能调用
emitAsset
等接口,这会导致style-loader
这一类 Loader 无法正常工作,解决方案是将这类组件放置在thread-loader
之前,如['style-loader', 'thread-loader', 'css-loader']
- Loader 中不能获取
compilation
、compiler
等实例对象,也无法获取 Webpack 配置
这会导致一些 Loader 无法与 Thread-loader 共同使用,读者需要仔细加以甄别、测试。
2. 使用 Parallel-Webpack
Thread-loader、HappyPack 这类组件所提供的并行能力都仅作用于执行加载器 —— Loader 的过程,对后续 AST 解析、依赖收集、打包、优化代码等过程均没有影响,理论收益还是比较有限的。对此,社区还提供了另一种并行度更高,以多个独立进程运行 Webpack 实例的方案 —— Parallel-Webpack。
1) 原理
parallel-webpack 的实现非常简单,基本上就是在 Webpack 上套了个壳,核心逻辑:
- 根据传入的配置项数量,调用
worker-farm
创建复数个工作进程 - 工作进程内调用 Webpack 执行构建
- 工作进程执行完毕后,调用
node-ipc
向主进程发送结束信号
到这里,所有工作就完成了。
2) 缺点
虽然,parallel-webpack 相对于 Thread-loader、HappyPack 有更高的并行度,但进程实例与实例之间并没有做任何形式的通讯,这可能导致相同的工作在不同进程 —— 或者说不同 CPU 核上被重复执行。例如需要对同一份代码同时打包出压缩和非压缩版本时,在 parallel-webpack 方案下,前置的资源加载、依赖解析、AST 分析等操作会被重复执行,仅仅最终阶段生成代码时有所差异。
这种技术实现,对单 entry 的项目没有任何收益,只会徒增进程创建成本;但特别适合 MPA 等多 entry 场景,或者需要同时编译出 esm、umd、amd 等多种产物形态的类库场景。
3. 并行压缩
Webpack 语境下通常使用 Uglify-js、Uglify-es、Terser 做代码混淆压缩,三者都不同程度上原生实现了多进程并行压缩功能。
TerserWebpackPlugin 完整介绍:https://webpack.js.org/plugins/terser-webpack-plugin/
以 Terser 为例,插件 TerserWebpackPlugin 默认已开启并行压缩能力,通常情况下保持默认配置即 parallel = true
即可获得最佳的性能收益。开发者也可以通过 parallel
参数关闭或设定具体的并行进程数量,例如:
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: 2 // number | boolean
})
],
},
};