HappyPack能让Webpack在同一时刻处理多个任务,发挥多核CPU电脑的功能以提升构建速度。HappyPack将任务分解给多个子进程去并发执行,子进程处理完后再将结果发送给主进程。

需要注意的是:由于javascript是单线程模型,所以要想发挥多核CPU的功能,就只能通过多进程实现,而无法通过多线程实现。

HappyPack是让webpack对loader的执行过程,从单一进程形式扩展为多进程模式,也就是将任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。从而加速代码构建与DLL动态链接库结合来使用更佳。

happypackwebpack的一个插件,目的是通过多进程模型,来加速代码构建。

配置属性:

  • mpDir: 存放打包缓存文件的位置;
  • cache: 是否开启缓存,目前缓存如果开启,(注: 会以数量级的差异来缩短构建时间,很方便日常开发);
  • cachePath: 存放缓存文件映射配置的位置;
  • verbose: 是否输出过程日志;
  • loaders: 因为配置中文件的处理 loader 都指向了 happypack 提供的 loadr ,这里配置的对应文件实际需要运行的 loader。

由于有大量文件需要解析和处理,所以构建是文件读写和计算密集型的操作,特别是当文件数量变多后,Webpack构建慢的问题会显得更为严重。运行在Node.js之上的Webpack是单线程模型的,也就是说Webpack需要一个一个地处理任务,不能同时处理多个任务。

文件读写和计算操作是无法避免的,那能不能让Webpack在同一时刻处理多个任务,发挥多核CPU电脑的功能,以提升构建速度呢?

HappyPack就能让Webpack做到这一点,它将任务分解给多个子进程去并发执行,子进程处理完后再将结果发送给主进程。

需要注意:由于JavaScript是单线程模型,所以要想发挥多核CPU的功能,就只能通过多进程实现,而无法通过多线程实现。

使用HappyPack

对于分解任务和管理线程的事情,HappyPack都会帮我们做好,我们所需要做的只是接入HappyPack。接入HappyPack的相关代码如下:

const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HappyPack = require('happypack');

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        // 将对.js文件的处理转交给id为babel的HappyPack实例
        use: ['happypack/loader?id=babel'],
        // 排除 node_modules 目录下的文件,node_modules目录下的文件都采用了ES5语法,没必要再通过Babel去转换
        exclude: path.resolve(__dirname, 'node_modules'),
      },
      {
        // 把对 .css 文件的处理转交给 id 为 css 的 HappyPack 实例
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          use: ['happypack/loader?id=css'],
        }),
      },
    ]
  },
  plugins: [
    new HappyPack({
      // 用唯一的标识符id来代表当前的HappyPack是用来处理一类特定的文件
      id: 'babel',
      // 如何处理.js 文件,用法和 Loader 配置中一样
      loaders: ['babel-loader?cacheDirectory'],
      // ... 其它配置项
    }),
    new HappyPack({
      id: 'css',
      // 如何处理.css文件,用法和Loader配置中一样
      loaders: ['css-loader'],
    }),
    new ExtractTextPlugin({
      filename: `[name].css`,
    }),
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

在以上代码中有以下两项重要的修改:

  1. Loader配置中,对所有文件的处理都交给了happypack/loader,使用紧跟其后的querystring?id=babel去告诉happypack/loader选择哪个HappyPack 实例处理文件;
  2. Plugin配置中新增了两个HappyPack实例,分别用于告诉 happypack/loader去如何处理.js和.css文件。选项中的id属性的值和上面 querystring中的?id=babel对应,选项中的loaders属性和Loader配置中的一样。

在实例化HappyPack插件时,除了可以传入id和loaders两个参数,HappyPack 还支持如下参数:

  • threads: 代表开启几个子进程去处理这一类型的文件,默认是3个,必须是整数;
  • verbose: 是否允许HappyPack输出日志,默认是true
  • threadPool: 代表共享进程池,即多个HappyPack实例都使用同一个共享进程池中的子进程去处理任务,以防止资源占用过多,相关代码如下:
const HappyPack = require('happypack');
// 构造出共享进程池,在进程池中包含5个子进程
const happyThreadPool = HappyPack.ThreadPool({ size: 5 });

module.exports = {
  plugins: [
    new HappyPack({
      // 用唯一的标识符 id 来代表当前的HappyPack用来处理一类特定的文件
      id: 'babel',
      // 如何处理 .js 文件,用法和 Loader 配置中一样
      loaders: ['babel-loader?cacheDirectory'],
      // 使用共享进程池中的子进程去处理任务
      threadPool: happyThreadPool,
    }),
    new HappyPack({
      id: 'css',
      // 如何处理 .css 文件,用法和 Loader 配置中一样
      loaders: ['css-loader'],
      // 使用共享进程池中的子进程去处理任务
      threadPool: happyThreadPool,
    }),
    new ExtractTextPlugin({
      filename: `[name].css`,
    }),
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

接入HappyPack后,需要给项目安装新的依赖:

npm i -D happypack
1

安装成功后重新执行构建,就会看到由HappyPack输出的以下日志: af42a9640b0ad674fed989eb730de4ea.png

说明HappyPack配置生效了,并且可以得知HappyPack分别启动了3个进程去并行处理任务。

HappyPack 原理

在整个Webpack构建流程中,最耗时的流程可能就是Loader对文件的转换操作了,因为要转换的文件数据量巨大,而且这些转换操作都只能一个一个地处理。HappyPack的核心原理就是:将这部分任务分解到多个进程中去并行处理,从而减少了总的构建时间。

从前面的使用中可以看出,所有需要通过Loader处理的文件都先交给了 happypack/loader去处理,在收集到了这些文件的处理权后,HappyPack就可以统一分配了。

每通过new HappyPack()实例化一个HappyPack,其实就是告诉HappyPack核心调度器如何通过一系列Loader去转换一类文件,并且可以指定如何为这类转换操作分配子进程。

核心调度器的逻辑代码在主进程中,也就是运行着Webpack的进程中,核心调度器会将一个个任务分配给当前空闲的子进程,子进程处理完毕后将结果发送给核心调度器,它们之间的数据交换是通过进程间的通信API实现的。

核心调度器收到来自子进程处理完毕的结果后,会通知Webpack该文件已处理完毕。

参考文档

  1. happypack 原理解析
  2. HappyPack