# 构建速度优化

使用 webpack 构建项目时,可以考虑一下构建速度的优化:

# 构建过程提速策略

# 1、不要让 loader 做太多事情

loader 功能很强大,但是也需要合理的使用,不要让 loader 做一些不必要的工作。

# babel-loader

https://v4.webpack.js.org/loaders/babel-loader/

  • 在使用 babel-loader 时,要确保转译尽可能少的文件。当使用 /\.m?js$/ 来匹配需要转译的文件类型时,也许会去转译 node_modules 目录或者其他不需要的源代码。对此,可以使用 exclude 来避免不必要的转译.
  • 另外也可以通过设置 cacheDirectory 选项为 true,将 babel-loader 提速至少两倍。这会将转译的结果缓存到文件系统中,之后的 webpack 构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的 Babel 重新编译过程(recompilation process)。
rules: [
  {
    test: /\.js$/,
    exclude: /(node_modules|bower_components)/,
    use: {
      loader: 'babel-loader',
      options: {
        presets: ['@babel/preset-env'],
        // This is a feature of `babel-loader` for webpack (not Babel itself).
        // It enables caching results in ./node_modules/.cache/babel-loader/
        // directory for faster rebuilds.
        cacheDirectory: true
      }
    }
  }
];

# 2、处理第三方库

第三方库以 node_modules 为代表,它们庞大得可怕,却又不可或缺。
处理第三方库的姿势有很多,其中,Externals 不够聪明,一些情况下会引发重复打包的问题;而 CommonsChunkPlugin 每次构建时都会重新构建一次 vendor;出于对效率的考虑,这里为大家推荐 DllPlugin
DllPlugin 是基于 Windows 动态链接库(Dynamic-link library)的思想被创作出来的。这个插件会把第三方库单独打包到一个文件中,这个文件就是一个单纯的依赖库。这个依赖库不会跟着你的业务代码一起被重新打包,只有当依赖自身发生版本变化时才会重新打包。

# DllPlugin

DllPlugin 处理文件,要分两步走:

  • 基于 dll 专属的配置文件,打包 dll 库
  • 基于 webpack.config.js 文件,打包业务代码

以一个基于 React 的简单项目为例,我们的 dll 的配置文件可以编写如下:

// webpack.dll.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    // 由依赖的库组成的数组
    vender: ['prop-types', 'babel-polyfill', 'react', 'react-dom', 'react-router-dom']
    // 也可以分开打包
    // babel: ['babel-polyfill'],
    // react: ['prop-types', 'react', 'react-dom', 'react-router-dom']
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js',
    library: '[name]_[hash]'
  },
  plugins: [
    new webpack.DllPlugin({
      // DllPlugin 的 name属性需要和 Libary 保持一致
      name: '[name]_[hash]',
      path: path.join(__dirname, 'dist', '[name]-mainfest.json'),
      // context 需要和 webpack.config.js 保持一致
      context: __dirname
    })
  ]
};

编写完成之后,运行这个配置文件,dist 文件夹里会出现这样两个文件:

vendor-manifest.json
vendor.js

vendor.js 是第三方库打包的结果。vendor-manifest.json,则用于描述每个第三方库对应的具体路径,下面截取一段看一下内容大概是什么样子:

{
  "name": "vendor_397f9e25e49947b8675d",
  "content": {
    "./node_modules/core-js/modules/_export.js": {
      "id": 0,
        "buildMeta": {
        "providedExports": true
      }
    },
    "./node_modules/prop-types/index.js": {
      "id": 1,
        "buildMeta": {
        "providedExports": true
      }
    },
    ...
  }
}

第一步,打包第三方库已经完成,下面来进行第二步:在 webpack.config.js 中配置 dll。

// webpack.config.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
  mode: 'production',
  // 编译入口
  entry: {
    main: './src/index.js'
  },
  output: {
    path: path.join(__dirname, 'dist/'),
    filename: '[name].js'
  },
  // dll 相关配置
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      // mainfest 就是第一步中打包出来的 json 文件
      mainfest: require('./dist/vendor-mainfest.json')
    })
  ]
};

一次基于 dll 的 webpack 构建过程优化,便大功告成了!

# 3、thread-loader(将 loader 由单进程转为多进程)

webpack 是单线程的,多个任务需要排队依次执行。thread-loader 可以开启一个线程池,让其他比较耗时的 loader 在这个线程池中工作。 它的用法就是把 thread-loader 放在其他 loader 前面就可以了,看下面的例子:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve('src'),
        use: [
          {
            loader: 'thread-loader',
            // loaders with equal options will share worker pools
            options: {
              // the number of spawned workers, defaults to (number of cpus - 1) or
              // fallback to 1 when require('os').cpus() is undefined
              workers: 2
            }
          }
          // your expensive loader (e.g babel-loader)
          "babel-loader"
        ]
      }
    ]
  }
};

babel-loader 功能强大,但相对耗时,例子中,将 thread-loader 添加在 babel-loader 之前,把 bable-loader 放在单独的线程池工作,提升了编译速度。