前端项目随着时间推移和业务发展,页面可能会越来越多,或者功能和业务代码会越来越多,又或者依赖的外部类库会越来越多,这个时候原本不足为道的 webpack 构建时间消耗就会慢慢地进入我们的视野。
构建消耗的时间变长了,如果是使用 CI 服务来做构建,大部分情况下我们无需等待,其实影响不大。但是本地的 webpack 开发环境服务启动时的速度和我们日常开发工作息息相关,在一些性能不是特别突出的设备上(例如便携式笔记本等等),启动时的长时间等待可能会让你越来越受不了。
博主亲身经历的一个项目,使用 webpack 构建的时长可以达到 6 分钟左右,这种场景下,就算用 CI 服务,在遇见需要紧急发布修复问题时,也会让人很抓狂。
也许某一天你负责的项目也会到了需要优化 webpack 构建性能的时候,所以这一章节我们来聊聊如何提升 webpack 的构建速度。
提升 webpack 构建速度本质上就是想办法让 webpack 少干点活,活少了速度自然快了,尽量避免 webpack 去做一些不必要的事情,记得这个主要方向,后续的针对构建速度的优化都是围绕着这一方向展开。
resolve
的解析在前边第三小节我们详细介绍了 webpack 的 resolve
配置,如果我们可以精简 resolve
配置,让 webpack 在查询模块路径时尽可能快速地定位到需要的模块,不做额外的查询工作,那么 webpack 的构建速度也会快一些,下面举个例子,介绍如何在 resolve
这一块做优化:
resolve: {
modules: [
path.resolve(__dirname, 'node_modules'), // 使用绝对路径指定 node_modules,不做过多查询
],
// 删除不必要的后缀自动补全,少了文件后缀的自动匹配,即减少了文件路径查询的工作
// 其他文件可以在编码时指定后缀,如 import('./index.scss')
extensions: [".js"],
// 避免新增默认文件,编码时使用详细的文件路径,代码会更容易解读,也有益于提高构建速度
mainFiles: ['index'],
},
上述是可以从配置 resolve
下手提升 webpack 构建速度的配置例子。
我们在编码时,如果是使用我们自己本地的代码模块,尽可能编写完整的路径,避免使用目录名,如:import './lib/slider/index.js'
,这样的代码既清晰易懂,webpack 也不用去多次查询来确定使用哪个文件,一步到位。
我们在使用 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 处理的话,构建速度也是会收很大影响的。
webpack 的 plugin 会在构建的过程中加入其它的工作步骤,如果可以的话,适当地移除掉一些没有必要的 plugin。
区分 mode 会让 webpack 的构建更加有针对性,更加高效。例如当 mode 为 development 时,webpack 会避免使用一些提高应用代码加载性能的配置项,如 UglifyJsPlugin,ExtractTextPlugin 等,这样可以更快地启动开发环境的服务,而当 mode 为 production 时,webpack 会避免使用一些便于 debug 的配置,来提升构建时的速度,例如极其消耗性能的 Source Maps 支持。
前边的内容我们提过,devtool 可以用于配置 webpack 构建出来的 sourcemap 相关的内容,输出详细的 sourcemap 是相当影响 webpack 的构建速度的,所以这里需要做一个取舍,在构建生产环境代码时不输出 sourcemap,而开发环境时一般选用 eval-cheap-source-map
来确保 sourcemap 基本可用的情况下还有着不错的构建速度。
thread-loader 是官方提供的一个可以利用多进程加速 loader 执行的 loader,如果项目中 loader 的运行处理占用比较多的时间,例如 babel 或者 typescript 解析和编译的代码量很大,或者 image—webpack-loader 处理图片耗时比较久,那么可以尝试使用 thread-loader 来提速。下边是基础使用的例子:
const threadLoader = require('thread-loader');
const threadLoaderOptions = {
// 这里填写对应 thread-loader 的配置
// 预热时的配置和使用 thread-loader 时的配置要一致,所以这里统一使用一个变量来管理
// 配置参考官方文档:https://github.com/webpack-contrib/thread-loader
}
// thread-loader 的预热,可以加速启动
threadLoader.warmup(threadLoader, [
'babel-loader',
// 更多其他需要使用 thread-loader 的 loader
]);
module.exports = {
// ...
module: {
rules: [
{
test: /\.jsx?/, // 匹配文件路径的正则表达式,通常我们都是匹配文件类型后缀
use: [
// 在你需要的 loader 前加上 thread-loader
{
loader: 'thread-loader',
options: threadLoaderOptions
},
{
loader: 'babel-loader', // 指定使用的 loader
options: {
presets: ['@babel/preset-env'],
},
},
]
},
// ...
],
}
值得注意的是,由于 thread-loader 新启动进程也需要耗时,所以如果不是运算特别多的 loader 使用 thread-loader 的话可能速度会更慢。
DLLPlugin 是 webpack 官方提供的一个插件,也是用来分离代码的,和 optimization.splitChunks
有异曲同工之妙,之所以把 DLLPlugin 放到 webpack 构建性能优化这一部分,是因为它的配置相对繁琐,如果项目不涉及性能优化这一块,基本上使用 optimization.splitChunks
即可。
我们来看一下 DLLPlugin 如何使用,使用这个插件时需要额外的一个构建配置,用来打包公共的那一部分代码,举个例子,假设这个额外配置是 webpack.dll.config.js
:
module.exports = {
name: 'vendor',
entry: ['react', 'react-dom'], // 这个例子我们打包 react & react-dom 作为公共类库
output: {
path: path.resolve(__dirname, "dist"),
filename: "vendor.js",
library: "vendor_[hash]" // 打包后对外暴露的类库名称
},
plugins: [
new webpack.DllPlugin({
name: 'vendor_[hash]',
path: path.resolve(__dirname, "dist/manifest.json"), // 使用 DLLPlugin 在打包的时候生成一个 manifest 文件
})
],
}
然后就是我们正常的应用构建配置,在那个的基础上添加两个一个新的 webpack.DllReferencePlugin
配置:
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, 'dist/manifest.json'),
// 指定需要用到的 manifest 文件,
// webpack 会根据这个 manifest 文件的信息,分析出哪些模块无需打包,直接从另外的文件暴露出来的内容中获取
}),
],
}
在构建的时候,我们需要优先使用 webpack.dll.config.js
来打包,如 webpack --config webpack.dll.config.js --mode production
,构建后生成公共代码模块的文件 vendor.js
和 manifest.json
,然后再进行应用代码的构建。
你会发现构建结果的应用代码中不包含 react 和 react-dom 的代码内容,这一部分代码内容会放在 vendor.js
这个文件中,而你的应用要正常使用的话,需要在 HTML 文件中按顺序引用这两个代码文件,如:
作用是不是和 optimization.splitChunks
很相似,但是有个区别,DLLPlugin 构建出来的内容无需每次都重新构建,后续应用代码部分变更时,你不用再执行配置为 webpack.dll.config.js
这一部分的构建,沿用原本的构建结果即可,所以相比 optimization.splitChunks
,使用 DLLPlugin 时,构建速度是会有显著提高的。
但是很显然,DLLPlugin 的配置要麻烦得多,并且需要关心你公共部分代码的变化,当你升级 lodash(即你的公共部分代码的内容变更)时,要重新去执行 webpack.dll.config.js
这一部分的构建,不然沿用的依旧是旧的构建结果,使用上并不如 optimization.splitChunks
来得方便。这是一种取舍,根据项目的实际情况采用合适的做法。
还有一点需要注意的是,html-webpack-plugin 并不会自动处理 DLLPlugin 分离出来的那个公共代码文件,我们需要自己处理这一部分的内容,可以考虑使用 add-asset-html-webpack-plugin,关于这一个的使用就不讲解了,详细参考官方的说明文档:使用 add-asset-html-webpack-plugin。
这里拿图片的压缩作为一个例子讲解,我们在前边的小节提到图片可以使用 webpack 的 image-webpack-loader 来压缩图片,在对 webpack 构建性能要求不高的时候,这样是一种很简便的处理方式,但是要考虑提高 webpack 构建速度时,这一块的处理就得重新考虑一下了,思考一下是否有必要在 webpack 每次构建时都处理一次图片压缩。
我们可以直接使用 imagemin 来做图片压缩,编写简单的命令即可。然后使用 pre-commit 这个类库来配置对应的命令,使其在 git commit
的时候触发,并且将要提交的文件替换为压缩后的文件。
这样提交到代码仓库的图片就已经是压缩好的了,以后在项目中再次使用到的这些图片就无需再进行压缩处理了,image-webpack-loader 也就没有必要了。
使用同样的思路去考虑其他构建步骤,是否有必要在每次构建时处理,可以考虑用 git hooks 或者工作流的一些机制来触发一些构建工作,来减少 webpack 的构建压力。
webpack 的构建性能优化是比较琐碎的工作,当我们需要去考虑 webpack 的构建性能问题时,往往面对的是项目过大,涉及的代码模块过多的情况。在这种场景下你单独做某一个点的优化其实很难看出效果,你可能需要从我们上述提到的多个方面入手,逐一处理,验证,有些时候你甚至会觉得吃力不讨好,投入产出比太低了,这个时候我们可以考虑换一个角度来思考我们遇到的问题。
例如,拆分项目的代码,根据一定的粒度,把不同的业务代码拆分到不同的代码库去维护和管理,这样子单一业务下的代码变更就无须整个项目跟着去做构建,这样也是解决因项目过大导致的构建速度慢的一种思路,并且如果处理妥当,从工程角度上可能会给你带来其他的一些好处,例如发布异常时的局部代码回滚相对方便等等。
官方发布的 webpack 4.0 更新日志来看,webpack 4.0 版本做了很多关于提升构建性能的工作,我觉得比较重要的改进有这么几个:
for of
替换 forEach
,用 Map
和 Set
替换普通的对象字面量等等,这一部分就不展开讲了,有兴趣的同学可以去 webpack 的 PRs 寻找更多的内容。更多关于性能优化的 webpack 迭代可以查看官方的 release 文档:webpack release,搜索 performance 关键字就好。
webpack 5 的大版本,针对性能优化还有更棒的持久化缓存方案推出,让每一次的构建打包可以向我们的 webpack-dev-server 一样,只针对变化部分做增量构建,可以极大地提高构建性能,这个值得期待一下,有兴趣的同学可以使用 webpack 5 beta 版本来测试一下。
本文我们首先介绍了如何从多个配置优化的方式来提高 webpack 的构建速度:
resolve
的解析在必要的时候,我们可以使用 thread-loader 和 DLLPlugin 来帮助我们进一步优化 webpack 的构建性能,但是从另外的角度考虑,在不过分依赖 webpack 构建的情况下,我们可以从流程优化上着手,如提交代码前就压缩图片,拆分构建的代码库等,以此来减少 webpack 构建的工作量。
同时,更新 Node 版本和 webpack 版本都有助于让我们的构建变得更快,勤劳的程序员们一直在优化代码库的性能,别辜负了他们的努力,尽可能让构建跑在最新的运行环境上吧。
你工作中遇到的项目是否有哪些部分是可以考虑使用上述的优化方法来提升构建速度的?你是否有其他的一些构建优化的方法呢?