提升 webpack 构建速度

1,444 阅读5分钟

这是我参与 8 月更文挑战的第 9 天,活动详情查看: 8月更文挑战

随着版本的迭代,业务的发展,代码会越来越多,这时候 webpack 的构建速度有可能就会变慢。

构建时间的长短直接关联着我们的工作效率,所以很有必要熟悉下如何提升 webpack 构建速度。

速度分析

在提升 webpack 构建速度之前,先使用 speed-measure-webpack-plugin 进行速度分析。 示例如下:

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

const smp = new SpeedMeasurePlugin();

const webpackConfig = smp.wrap({
  plugins: [new MyPlugin(), new MyOtherPlugin()],
});

速度分析

可以看到每个 loader 和插件执行耗行

使用高版本的 webpack 与 Node.js

这也是官网建议的方案,目前 webpack 已经更新到 5 了,但由于更新到 webpack5 ,一系列的 pluginloader 都要做相应的更新,所以如果是新项目建议用 webpack5,老项目就使用 webpack4,至于 4 以下就不建议了。

使用多进程/ 多实例构建

可选方案

使用 HappyPack 解析资源

原理:每次 webapck 解析一个模块,HappyPack 会将它及它的依赖分配给 worker 线程中

const HappyPack = require('happypack');
 
exports.module = {
  rules: [
    {
      test: /.js$/,
      // 1) replace your original list of loaders with "happypack/loader":
      // loaders: [ 'babel-loader?presets[]=es2015' ],
      use: 'happypack/loader',
      include: [ /* ... */ ],
      exclude: [ /* ... */ ]
    }
  ]
};
 
exports.plugins = [
  // 2) create the plugin:
  new HappyPack({
    // 3) re-add the loaders you replaced above in #1:
    loaders: [ 'babel-loader?presets[]=es2015' ]
  })
];

这个库作者已经不维护了,webpack4 后的推荐使用 thread-loader

使用 thread-loader 解析资源

原理:每次 webpack 解析一个模块,thread-loader 会将它及它的依赖分配给 worker 线程中

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve('src'),
        use: [
          'thread-loader',
          // your expensive loader (e.g babel-loader)
        ],
      },
    ],
  },
};

多进程/多实例:并行压缩

方法一:使用 parallel-uglify-plugin 插件

import ParallelUglifyPlugin from 'webpack-parallel-uglify-plugin';

module.exports = {
  plugins: [
    new ParallelUglifyPlugin({
      uglifyJS: {
        output: {
          beautify: false,
          comments: false
        },
        comperss: {
          // 是否在UglifyJS删除没有用到的代码时输出警告信息,默认为输出,可以设置为false关闭这些作用
          warnings: false,
          drop_console: true,
          collapse_vars: true,
          reduce_vars: true
        }
      },
    }),
  ],
};

方法二:uglifyjs-webpack-plugin 开启 paralle

const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
  plugins: [
    new UglifyJsPlugin({
      uglifyOptions: {
        warnings: false,
        parse: {},
        compress: {},
        mangle: true,
        output: null,
        toplevel: false,
        nameCache: null,
        ie8: false,
        keep_fnames: false
      },
      parallel: true
    })
  ]
}

方法三:terser-webpack-plugin 开启 parallel 参数(推荐使用)

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

让 webpack 少做点事

webpack 少做点事,自然构建速度就提升了

减少 resolve 的解析

通过精简 resolve 配置,让 webpack 在查询模块路径时尽可能快速地定位到需要的模块,不做额外的查询工作,那么 webpack 的构建速度也会快一些。

比如:

resolve: {
  modules: [
    path.resolve(__dirname, 'node_modules'), // 使用绝对路径指定 node_modules,不做过多查询
  ],
  // 删除不必要的后缀自动补全,少了文件后缀的自动匹配,即减少了文件路径查询的工作
  // 其他文件可以在编码时指定后缀,如 import('./index.scss')
  extensions: [".js"], 
  // 避免新增默认文件,编码时使用详细的文件路径,代码会更容易解读,也有益于提高构建速度
  mainFiles: ['index'],
}

在编码时,如果是使用我们自己本地的代码模块,尽可能编写完整的路径,避免使用目录名,如:import './lib/slider/index.js',这样的代码既清晰易懂,webpack 也不用去多次查询来确定使用哪个文件,一步到位。

loader 应用的文件范围缩小

在使用 loader 的时候,尽可能把 loader 应用的文件范围缩小,只在最少数必须的代码模块中去使用必要的 loader,例如 node_modules 目录下的其他依赖类库文件,基本就是直接编译好可用的代码,无须再经过 loader 处理了:

rules: [ 
  {
    test: /\.jsx?/,
    include: [ 
      path.resolve(__dirname, 'src'), 
      // 限定只在 src 目录下的 js/jsx 文件需要经 babel-loader 处理
      // 通常我们需要 loader 处理的文件都是存放在 src 目录
    ],
    use: 'babel-loader',
  },
  // ...
],

如上边这个例子,如果没有配置 include,所有的外部依赖模块都经过 Babel 处理的话,构建速度也是会收很大影响的。

使用 DLLPlugin

  • 需求:将 react react-dom redux 等基础包和业务基础包打成一个文件
  • 方法: 使用 DLLPlugin 进行分包,DllReferencePluginmanifest.json 引用
  1. 需要新建个构建配置文件,比如是 webpack.dll.config.js
module.exports = {
  context: process.cwd(),
  entry: {
    library: [
      'react',
      'react-dom',
      'redux',
      'react-redux'
    ],
    // 如果有多个,直接在增加一个
  }
  output: {
    // 这里打包后的文字是 library.dll.js
    filename: '[name].dll.js',
    path: path.resolve(__dirname, 'build/libarary'),
    // 暴露的库的名字
    library: '[name]'
  },
  plugins: [
    // 指定包存放的位置
    new webpack.DllPlugin({
      name: '[name]',
      // 描述动态链接库 mainfest 文件输出时的文件名称
      // path: 'manifest.json'
      path: path.resolve(__dirname, 'build/libarary/[name].json')
    })
  ]
}
  1. pageage.json scripts 中增加命令
"scripts": {
  "dll": "webpack --config webpack.dll.js"
}
  1. 执行 npm run dll 分包
  2. webpack.config.js 引入
module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      // 刚打包后的json文件地址
      // manifest: require('xxx.json'),
      manifest: require('./build/library/libary.json'),
      // 指定需要用到的 manifest 文件,
      // webpack 会根据这个 manifest 文件的信息,分析出哪些模块无需打包,直接从另外的文件暴露出来的内容中获取
    })
    // 如果引入多个,使用多次此插件
  ]
}

作用和 optimization.splitChunks 很相似,但是有个区别,DLLPlugin 构建出来的内容无需每次都重新构建,后续应用代码部分变更时,你不用再执行配置为 webpack.dll.config.js 这一部分的构建,沿用原本的构建结果即可,所以相比 optimization.splitChunks,使用 DLLPlugin 时,构建速度是会有显著提高的。

但是很显然,DLLPlugin 的配置要麻烦得多,并且需要关心你公共部分代码的变化,当你的公共部分代码的内容变更时,要重新去执行 webpack.dll.config.js 这一部分的构建,不然沿用的依旧是旧的构建结果,使用上并不如 optimization.splitChunks 来得方便。这是一种取舍,根据项目的实际情况采用合适的做法。

利用缓存提升构建速度

通过开启缓存来提升二次构建速度,开启缓存后 node_modules 下会有一个 .cache 目录

babel-loader 开启缓存

use: [
  {
    loader: 'babel-loader',
    options: {
      cacheDirectory: true
    }
  }
]

terser-webpack-plugin 开启缓存

module.exports = {
  optimization: {
    minimizer: [
      new Terserplugin({
        parallel: true,
        cache: true
      })
    ]
  }
}

使用 hard-source-webpack-plugin 开启缓存

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

plugins: [
  new HardSourceWebpackPlugin()
]