使用 HappyPack 和 DllPlugin 来提高你的 Webpack 构建速度

本文原文发表在:https://medium.com/@Erichain/...
本文采用的 Webpack 版本为 2.0+
本文源代码地址:https://github.com/Erichain/w...javascript

若是你问我对 Webpack 什么印象的话,我只能告诉你,慢,真的慢。即便他的配置如文档所说(固然,它的文档也不是那么好)很简单,不像 Grunt 或者 Gulp 那样须要一堆配置,只需那么几十行就可以配置一个构建系统,我依然以为,这个构建工具很慢。或许,是从它的文档开始,我就印象很差了?OK,这个话题到此为止,咱们开始咱们的正题吧。css

本篇文章面向的不是 Webpack 新手,若是你对 Webpack 还不太熟悉的话,建议去阅读它的官方文档。固然,咱们确定也会涉及到一些基础的东西。java

本文重点讲解对生产环境的构建的性能的提高,若是须要对本地构建的性能进行提高的话,能够在本文结束以后,本身寻找一下解决方案哦。固然,还有一点须要说明的是,本文中的代码在本地不必定真正可以在浏览器中运行,有须要的能够自行搭建本地的构建系统。react


一点基础

使用过 Webpack 的朋友确定知道,Webpack 的最简单的配置以下:webpack

module.exports = {
  entry: {
    app: './src/app.js'
  },
  
  output: {
    path: path.join(__dirname, 'dist-[hash]'),
    filename: '[name].[hash].js'
  }
};

这样的配置会将咱们的文件打包成为一个 app.[hash].js 文件。这样针对的通常是咱们的项目不算大的状况,而且公用模块比较少的状况(固然,公用模块较多的话,配置确定也不会这么简单了)。git

对于项目中有用到预处理器,ES2015+ 或者其他的须要编译后在浏览器上运行的语言,咱们须要作的就是为这些东西添加上对应的 loader,而后,Webpack 就会自动的帮咱们进行处理了(老实说,这一步仍是挺方便的)。github

一些 loader 配置示例以下:web

rules: [
  {
    test: /\.jsx?$/,
    loader: ['babel-loader?presets[]=react,presets[]=latest&compact=false'],
  }, {
    test: /\.scss$/,
    loader: [
      'style-loader',
      'css-loader',
      'postcss-loader',
      'sass-loader'
    ],
  }, {
    test: /\.jpe?g|png|svg|gif/,
    loader: ['url-loader?limit=8192&name=assets/images/[name]-[hash].[ext]'],
  }
]

另外,咱们还能够经过一些插件来更多的定义 Webpack 的打包行为。好比,若是咱们有不少第三方库的引用,而且,多个地方都会引用到这些库,咱们就可使用 Webpack 的 CommonsChunkPlugin 来将这些公用的代码打包成一个文件(固然,至于速度嘛,咱们后面再说),而后,将咱们页面的业务代码打包成为一个文件。npm

Webpack 的主要配置就这几项,其余更多的更深刻的配置能够查看 Webpack 的官方文档json

速度慢

尽管 Webpack 配置起来很方便,可是,按照通常的配置来的话,构建的速度真的是太慢了,每构建一次都会花掉至关长的时间,这对于开发者们来讲简直是噩梦。

但是,速度为何会这么慢呢?

以我所在的项目为例,因为咱们的项目存在多个 entries(大概四十多个),因此,咱们的 Webpack 采用的配置是将公用的第三方库经过 CommonsChunkPlugin 来打包成为一个 common.js

根据这个 common.js 的内容来看,这里面存放的就是各个 entry 引用的公有的代码,好比,咱们的不少组件都会用到 React 或者 Redux 这些第三方库。经过将公有的代码单独打包成一个文件,而后再将业务代码打包成一个文件,这样一来,业务代码模块自己的体积就会减少不少,页面的加载速度也可以获得很大的提高。

虽然这样打包的方式可以在必定程度上提高页面的加载速度,可是,咱们简单的想想也知道,CommonsChunkPlugin 会去将全部 entry 中的公有模块遍历出来再进行编译压缩混淆,这个过程是很是缓慢的(咱们的项目之前在使用这种方式的时候,在这一步会花上至少十二分钟的时间,你能够想象这个过程有多么漫长)。

通过了几个迭代的痛苦的打包上线的过程以后,咱们终于不能忍了,决定对这个构建系统进行改造。

改造的过程

说实话,一开始我实际上是没有任何头绪的,我只知道这个构建的过程慢,可是,并不清楚应该从何处开始进行改造。

与同事们进行了一些商讨以后,我准备从如下几个方面入手:

  • 减小构建的文件,减少文件大小:咱们的项目中存在太多的无用的文件和代码,我决定先删除这些无用的东西

  • 移除 CommonsChunkPlugin

  • Search with Google

第一步的做用其实并不明显,我删除了很大一部分的无用的图片和代码,可是,构建速度并无明显的提高。

第二步,简单的移除掉 CommonsChunkPlugin 的话,构建速度确实会快不少,可是,这样打包出来的项目就不可以运行了,因此,还须要结合第三步(必需要感谢这个世界存在 Google)。

我在网上找到了许多相关的问题,关键性的建议有如下几个:

  • css-loader 的版本回溯到 0.15 及其之前的版本

  • 使用 HappyPack

  • 使用 DllPlugin

首先,第一点,下降 css-loader 的版本。

在 GitHub 上有这样一个 issue:0.15.0+ makes Webpack load slowly。按照 issue 中你们的讨论,我将咱们项目中的 css-loader 的版本降到了 0.14.5。满怀期待的觉得这样就可以提高一部分速度,可是,结果是使人失望的——构建的速度并无明显的改变。我试着构建了好几遍,速度依然没有提高,因此,第一个方法失败,我将 css-loader 的版本恢复了回来。

那么,继续尝试第二个方法,也是本文将要重点说明的方法之一,那就是使用 HappyPack。

使用 HappyPack

HappyPack 容许 Webpack 使用 Node 多线程进行构建来提高构建的速度。

使用的方法与在 Webpack 中定义 loader 的方法相似,只是说,咱们把构建须要的 loader 放到了 HappyPack 中,让 HappyPack 来为咱们进行相应的操做,咱们只须要在 Webpack 的配置中引入 HappyPack 的 loader 的配置就行了。

好比,咱们编译 .jsx 文件的 loader 就能够这样写:

new HappyPack({
  id: 'jsx',
  threads: 4,
  loaders: ['babel-loader?presets[]=react,presets[]=latest&compact=false'],
})

其中,threads 指明 HappyPack 使用多少子进程来进行编译,通常设置为 4 为最佳。

编译 .scss 文件的 loader 这样写:

new HappyPack({
  id: 'scss',
  threads: 4,
  loaders: [
    'style-loader',
    'css-loader',
    'postcss-loader',
    'sass-loader',
  ],
})

其中,须要注意的一点就是,在使用 HappyPack 的状况下,咱们须要单首创建一个 postcss.config.js 文件,否则,在编译的时候,就会报错。

因为 HappyPack 对 url-loaderfile-loader 的支持度的问题,因此,咱们此处,打包图片文件的时候,并无使用 HappyPack。

postcss.config.js 的配置就像下面这样(根据你的需求,定制你本身的配置):

module.exports = {
  autoprefixer: {
    browsers: ['last 3 versions'],
  }
};

定义好了咱们 HappyPack 的 loader 以后,咱们直接在咱们的 Webpack 的配置的 plugins 一项中,引入就行了。

那么,咱们在编译的时候,就会看到下面的输出:

@HappyPack 输出|center

这就是 HappyPack 在编译的时候的输出内容。

可是,咱们的关注点不是它输出了什么,而是说,咱们的构建速度有没有提高。

固然,结果是使人失望的,咱们单独使用 HappyPack 的状况下,构建速度并无明显的提高(固然,或许有所提高可是我没有发现也有可能)。

因此,为了进一步的提高咱们的构建速度,咱们将采起第三种方案,那就是 DllPlugin。

使用 DllPlugin

仔细阅读过 Webpack 文档的朋友确定对这个插件会有印象,或者说知道这个插件是干吗用的。其实,咱们此处也是基于 Webpack 的文档的一些说明,而后,结合我在项目中的实践来为你们讲解这个插件。

在 Webpack 中,DllPlugin 并非单独的使用的,而是须要与一个名为 DllReferencePlugin 的插件结合起来使用的。

熟悉 Windows 的朋友就应该知道,DLL 所表明的含义。在 Windows 中,有大量的 .dll 文件,称为动态连接库。

MSDN 上,微软是这样解释动态连接库的:

A dynamic-link library (DLL) is a module that contains functions and data that can be used by another module (application or DLL).

大概的意思就是说,动态连接库包含的是,能够在其余模块中进行调用的函数和数据。

文档里面还有一句话是这样说的:

DLLs provide a way to modularize applications so that their functionality can be updated and reused more easily.

动态连接库提供了将应用模块化的方式,应用的功能能够在此基础上更容易被复用。

回到咱们的项目中,相似的,咱们其实要作的也是将各个模块中公用的部分给打包成为一个公用的模块。这个模块就包含了咱们的其余模块中须要的函数和数据(好比,其余组件所需的 React 库)。

使用 DllPlugin 的时候,会生成一个 manifest.json 这个文件,所存储的就是各个模块和所需公用模块的对应关系。

说了这么多,咱们不如直接来看看这个插件究竟是怎么使用的:

首先,咱们须要一个文件,这个文件包含全部的第三方或者公用的模块和库,咱们在此将其命名为 vendor.js,文件的内容以下:

import 'react';
import 'react-dom';

因为咱们的示例项目中只用到了这两个公用的第三方库,因此,咱们此处只须要引入这两个库就好了。

在打包的时候,咱们将这些公用的模块单独打包成一个文件,而后,经过生成的 manifest.json 文件对应过去。因此,咱们须要单首创建一个 webpack.config.vendor.js

文件内容其实很简单:

const webpack = require('webpack');
const path = require('path');

module.exports = {
  entry: {
    vendor: [path.join(__dirname, 'src', 'vendor.js')],
  },

  output: {
    path: path.join(__dirname, 'dist-[hash]'),
    filename: '[name].js',
    library: '[name]',
  },

  plugins: [
    new webpack.DllPlugin({
      path: path.join(__dirname, 'dll', '[name]-manifest.json'),
      filename: '[name].js',
      name: '[name]',
    }),
  ]
};

能够看到,咱们主要的操做是在 plugins 配置中,生成的文件名就是咱们所定义的 entry 的名称,JSON 文件名能够根据本身的须要来命名。像上面这样,咱们就能够将咱们的一些公用模块打包出来了。

运行如下命令:

webpack -p --progress --config webpack.config.vendor.js

咱们就能够看到这样的输出:

@DllPlugin 打包输出|center

这样,咱们就完成了构建的第一步。下一步,咱们须要在构建应用的配置文件中,加入咱们的 DllPlugin 的配置。

这时候,咱们就须要用到 DllReferencePlugin 了。

在咱们的主要配置文件中,加入如下的配置:

const manifest = require('./dll/vendor-manifest.json');

// ... 其余完美的配置

plugins: [
  new webpack.DllReferencePlugin({
    manifest,
  }),
],

就这样,咱们的全部工做就完成了,咱们只须要运行一条命令,就可以看到构建速度的巨大提高。

固然,为了更完美,咱们能够将 DllPlugin 和 HappyPack 结合起来使用,效果会更好。具体的代码细节,此处不予展现,朋友们能够直接去 GitHub 上查看。

为了方便构建,咱们能够写一个脚本将构建过程简单化。在个人 GitHub 项目里面有相关的脚本,包含了一些基础的操做,有须要的朋友能够去查看。此处,咱们就认为咱们的命令能够直接构建了。

为了体现出构建速度的区别,咱们先运行 npm run build,这是采用普通方式进行构建的命令。

@采用普通构建方式的构建时间|center

能够看到,构建时间为 20353ms,换算下来为 20s 左右。

接下来,咱们运行 npm run build dll,经过 DllPlugin 和 HappyPack 进行构建。

@构建 vendor.js 文件的时间|center

@构建 app.js 文件的时间|center

咱们将两个时间加起来,总共为 12184ms,换算下来为 12s 左右。快了将近一倍的时间!这还只是文件少的状况。在咱们的实际项目中,构建时间提高了 3 倍多,因此,能够看到 DllPlugin 的强大之处。

一点总结

本文只是寻找了这样几种可以提高构建速度的解决方案,我相信,方法确定不止这些,必定还有更多的解决方案等待咱们去发现。因此,但愿各位朋友可以对本文中不足的地方提出建议,但愿与你们共同窗习,共同进步。


References

OPTIMIZING WEBPACK FOR FASTER REACT BUILDS

Optimizing Webpack build times and improving caching with DLL bundles

Dynamic-Link Libraries