Skip to main content

多进程打包

受限于 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 中不能获取 compilationcompiler 等实例对象,也无法获取 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 做代码混淆压缩,三者都不同程度上原生实现了多进程并行压缩功能。

tip

以 Terser 为例,插件 TerserWebpackPlugin 默认已开启并行压缩能力,通常情况下保持默认配置即 parallel = true 即可获得最佳的性能收益。开发者也可以通过 parallel 参数关闭或设定具体的并行进程数量,例如:

const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: 2 // number | boolean
})
],
},
};

参考

Webpack 性能系列二:多进程打包