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 主进程,从而实现性能更佳的文件加载转译效果

先看下用法:

  1. 需要放在 use 数组首位
    1. module.exports = {
    2. module: {
    3. rules: [
    4. {
    5. test: /\.js$/,
    6. use: ["thread-loader", "babel-loader", "eslint-loader"],
    7. },
    8. ],
    9. },
    10. };
    thread-loader还有一些配置:
  • workers:子进程总数,默认值为 require(‘os’).cpus() - 1;
  • workerParallelJobs:单个进程中并发执行的任务数;
  • poolTimeout:子进程如果一直保持空闲状态,超过这个时间后会被关闭;
  • poolRespawn:是否允许在子进程关闭后重新创建新的子进程,一般设置为 false 即可;
  • workerNodeArgs:用于设置启动子进程时,额外附加的参数。
    1. module.exports = {
    2. module: {
    3. rules: [
    4. {
    5. test: /\.js$/,
    6. use: [
    7. {
    8. loader: "thread-loader",
    9. options: {
    10. workers: 2, // 子进程总数
    11. workerParallelJobs: 50, // 单个进程中并发执行的任务数
    12. // ...
    13. },
    14. },
    15. "babel-loader",
    16. "eslint-loader",
    17. ],
    18. },
    19. ],
    20. },
    21. };
    warmup 配置:为防止启动工作程序时出现高延迟,可以预热工作程序池。这将引导池中的最大工作程序数量,并将指定的模块加载到node.js模块高速缓存中。(类似线程池) ```javascript const threadLoader = require(“thread-loader”);

threadLoader.warmup( { // 可传入上述 thread-loader 参数 workers: 2, workerParallelJobs: 50, }, [ // 子进程中需要预加载的 node 模块 “babel-loader”, “babel-preset-es2015”, “sass-loader”, ] );

  1. threadLoader有两个突出的优点,一是官方团队出品,稳定性有保障;二是用法更简单。<br />但它也存在一些问题:
  2. 1. Thread-loader 中运行的 Loader 不能调用 emitAsset 等接口,这会导致 style-loader 这一类加载器无法正常工作(解决方案是将这类组件放置在 thread-loader 之前,如 ['style-loader', 'thread-loader', 'css-loader'] );
  3. 2. Loader 中不能获取 compilationcompiler 等实例对象,也无法获取 Webpack 配置。
  4. 比如,配合babel-loader没啥问题:
  5. ```javascript
  6. {
  7. test: /\.jsx$/,
  8. use: [
  9. "thread-loader",
  10. {
  11. loader: "babel-loader",
  12. options: {
  13. presets: ["@babel/preset-react"],
  14. },
  15. }
  16. ]
  17. },

但是配合ts-loader就有点问题:

  1. {
  2. test: /\.tsx$/,
  3. use: ["thread-loader", "ts-loader"],
  4. },

happyPack

HappyPack :文件加载(Loader)操作拆散到多个子进程中并发执行,子进程执行完毕后再将结果合并回传到 Webpack 进程,从而提升构建性能。
其使用方法:

  • 使用 happypack/loader 代替原本的 Loader 序列;
  • 使用 HappyPack 插件注入代理执行 Loader 序列的逻辑
  1. 使用 happypack/loader :

    1. module.exports = {
    2. // ...
    3. module: {
    4. rules: [
    5. {
    6. test: /\.js$/,
    7. use: "happypack/loader",
    8. // 原始配置如:
    9. // use: [
    10. // {
    11. // loader: 'babel-loader',
    12. // options: {
    13. // presets: ['@babel/preset-env']
    14. // }
    15. // },
    16. // 'eslint-loader'
    17. // ]
    18. },
    19. ],
    20. },
    21. };
  2. 使用 happypack 插件实例new HappyPack():

    1. plugins: [
    2. new HappyPack({
    3. // 将原本定义在 `module.rules.use` 中的 Loader 配置迁移到 HappyPack 实例中
    4. loaders: [
    5. {
    6. loader: "babel-loader",
    7. option: {
    8. presets: ["@babel/preset-env"],
    9. },
    10. },
    11. "eslint-loader",
    12. ],
    13. }),
    14. ],

    可以看到,原来的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’] }) ] };

  1. > 多实例模式虽然能应对多种类型资源的加载需求,但默认情况下,HappyPack 插件实例 自行管理 自身所消费的进程,需要导致频繁创建、销毁进程实例 —— 这是非常昂贵的操作,反而会带来新的性能损耗。
  2. 针对这种频繁创建销毁问题的通用解法就是池,所以happyPack也有类似的进程池机制:HappyPack.ThreadPool
  3. ```javascript
  4. const happyThreadPool = HappyPack.ThreadPool({
  5. // 设置进程池大小
  6. size: os.cpus().length - 1
  7. });

声明进程池后,需要在每一个plugin实例中关联:

  1. plugins: [
  2. new HappyPack({
  3. id: 'js',
  4. // 设置共享进程池
  5. threadPool: happyThreadPool,
  6. loaders: ['babel-loader', 'eslint-loader']
  7. }),
  8. new HappyPack({
  9. id: 'styles',
  10. threadPool: happyThreadPool,
  11. loaders: ['style-loader', 'css-loader', 'less-loader']
  12. })
  13. ]

HappyPack 虽能有效提速,但它存在明显问题:

  1. 作者已不维护;
  2. 可能存在一些意想不到的兼容性问题(HappyPack 底层以自己的方式重新实现了加载器逻辑,源码与使用方法都不如 Thread-loader 清爽简单,而且会导致一些意想不到的兼容性问题,如 awesome-typescript-loader);
  3. 作用范围单一,性能收益有限。HappyPack 主要作用于文件加载阶段。

    parallel-webpack

    这是一种并行度更高,以多个独立进程运行 Webpack 实例的方案。(上面的threadLoader、happy Pack都是load资源时,文件加载过程的并行方案)
    使用方法:
    1. module.exports = [
    2. {
    3. entry: 'pageA.js',
    4. output: {
    5. path: './dist',
    6. filename: 'pageA.js'
    7. }
    8. },
    9. {
    10. entry: 'pageB.js',
    11. output: {
    12. path: './dist',
    13. filename: 'pageB.js'
    14. }
    15. }
    16. ];
    build时需要在package.json的script中替换原来的webpack为parallel-webpack。
    更多用法参考:https://github.com/trivago/parallel-webpack

    terser-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 })], }, }; ```