Webpack 构建性能优化实践
随着项目体积膨胀,webpack 构建时间越来越长。webpack 构建属于高频需求,每天耗费大量时间干等着是很难受的——于是有了本文,探讨如何加速 webpack 构建。
1. 使用 visualizer 追踪与精简项目依赖
项目本身容易出现的问题有以下两点:
- 为了实现小功能,引了个大包
比如:只为了使用 lodash
中的某个工具函数,把整个 lodash
都引进来;只为了一个简单的时间格式化功能,引了几十 K 的 moment
;只为了画一个简单的小图表,引了整个 echarts
。
为了一个小功能,引入一个复杂的库明显是不划算的。为了解决这种问题, 我们需要仔细分析项目的依赖:lodash
有打碎的工具函数的各自的库,需要时可以直接引用 lodash.clonedeep
库,避免引入无用内容;简单功能自己实现,不要太偷懒;需求不高,可以换用小体积的库,比如画简单图,完全可以将 echarts
替换成能达到功能的另一个小型库。
- 无用依赖忘记移除
有时业务代码变动,部分逻辑下线,而文件头部的模块引入部分忘了删掉,就会导致产生无用依赖。
优化精简依赖既可以减少打包时间,也可以优化 bundle 体积。
项目使用的依赖散落在文件各处,其体积也难以横向对比。借助 webpack 的 stat 功能与合适的 visualizer 工具可以更轻松的分析依赖。
1.1 输出 stats.json
使用 webpack-stats-plugin 插件可以在构建完成后的 output 目录下生成 stats.json
文件。这个文件记录了 webpack 构建相关数据。
webpack-stats-plugin
的使用方法可见 README.md。
1.2 将 stats.json 导入 visualizer
目前有两个好用的 visualizer:
通过这个圆环图可以清晰的看出各依赖的占比,寻找其中的大块依赖,检查是否有可精简项。
webpack 官方出的分析工具,可以从更多维度分析依赖。可以清晰的看到各文件中的依赖关系。
2. 升级环境
一般来说新版本总是要胜于旧版本,将 node、npm 以及项目依赖在项目正常工作的前提下升级到尽可能新的稳定版,往往会有好结果。例如:伴随着 node 8 发布的 npm 5 就声称在性能上获得了显著提升,至少 npm install
的速度起飞了。
3. 提速 Webpack 构建
3.1 查看 Webpack 各环节消耗时间
使用命令行调用 Webpack 时加入 --colors
、--profile
、--display-modules
可以显示加载的模块、构建各环节耗时,并着色以增强可读性。
3.2 使用 dllPlugin & DllReferencePlugin
这两个插件搭配使用,作用是将不产生变化的公共库(如 vue、react、polyfill 这种)抽离,将其进行预编译,这样在每次构建时就不再重复分析这些公共库,避免内耗。dllPlugin 用于预编译公共库,产生相应的 dll.js
和 manifest.json
,DllReferencePlugin 用于将 dllPlugin 产生的预编译结果传送给 Webpack。使用 DllReferencePlugin 前要先用 dllPlugin 准备好预编译结果。
3.2.1 使用 dllPlugin 做预编译
新建一个 webpack.config.dll.js
,其内容与项目构建文件基本类同,只是将入口换成公共库,仅对公共库做编译:
|
|
编写完后执行 webpack --config=webpack.config.dll.js
,没问题的话,预编译结果会在 /dll
下会生成。
3.2.2 使用 DllReferencePlugin 引入预编译库 manifest
在项目构建的 webpack.config.js
中加入:
|
|
3.2.3 在页面中引入 dll.js
在 index.html 中,引入项目主要的 app.js
之前,先引入 dll.js
即可。如: <script src="../dll/dll.vendor.js"></script>
。
如果使用 HtmlwebpackPlugin
插件,可以搭配 HtmlWebpackIncludeAssetsPlugin
插件引入该 js:
|
|
这就大功告成了。项目中依赖的公共库越多,提速效果越大,一般来说这个优化项都可带来 20%+ 的提速效果。我们再次使用 webpack 打包,可以看到:
公共库全部标明 from dll-reference vendor
,同时其 building
耗时均为 0-1ms。相比较上面的几个 js,building
动辄 500ms 左右。这部分时间被节约了出来。
由于做了 vendor 的拆分,最终实际上是拆成了两个包,如果生产环境下还是想只用一个包的话,可以将这个插件只应用于开发环境,不用于生产环境。
3.3 happypack
webpack 在单个 node 进程中调用 loader,导致耗 cpu 计算型 loader 性能不佳。happypack 允许多进程调用 loader,恰当的使用 happypack 可以提速构建。
该插件适合场景:项目复杂度高、耗 cpu 操作多(如 babel transform)、需处理文件多。如果项目简单,使用该插件效果即不显著。
用法非常简单,首先在 webpack 构建文件中添加该插件:
|
|
这里只允许 happypack 并行执行 babel-loader
,对其他 loader 做了尝试,提速不明显。各位也可以根据自己项目实际情况多试试。
在 webpack 打印出的 log 里可以确认 happypack 是否生效。
3.4 使用 webpack-uglify-parallel 并行执行压缩
webpack-uglify-parallel
功能与自带的 uglifyPlugin
没差,只是允许开启多核并行执行以提高性能。
|
|
直接将项目中的 uglifyPlugin
替换为 webpack-uglify-parallel
即可。多加一个参数 workers
,指定进程数,一般来说设置为 CPU 核心数。
3.5 其他零散的优化点
3.5.1 cache
开启 webpack 的 cache 选项:
|
|
开启 babel-loader 的 cache 选项:
|
|
3.5.2 resolve.modules
定义 resolve.modules 可以告知 webpack 去哪里搜索模块。显式定义可以节约搜索时间。
|
|
3.5.3 resolve.alias 与 module.noParse
显式指定外部依赖的具体文件,如:
|
|
指定到对应库的 min 文件,这种文件已经经过压缩,且不再含外部依赖,我们可以应用 noParse 让 webpack 跳过对其的解析来进行优化:
|
|
4. CommonsChunkPlugin 与 DllPlugin 的区别
在多入口项目中,每个入口最终会产生一个独立的 bundle。CommonsChunkPlugin 可以抽取多入口中的公共模块部分,将公共模块打包到一个或多个独立文件中去,以便在其他入口和模块中使用。
使用 CommonsChunkPlugin 主要是为了复用公共模块,减少最终打包文件体积,优化加载性能。公共模块在每次构建时都会重新打包,所以并不会提升构建速度。
DllPlugin 的目的是预编译公共库,这样在之后的构建过程中,公共库都不需重新打包,主要目的是加速构建过程。当然,他也可以在生产环境临时客串一下 CommonsChunkPlugin 的功能,把你的代码拆分成两部分,dll 和其他,如果你想这么做的话。
5. 试验
在一个手头的小项目上,试验上面的方法,每次打包时间重复三次取平均值:
生产环境(ms) | 开发环境(ms) | |
---|---|---|
优化前 | 15509 | 11226 |
dllPlugin | 11166 | 8788 |
happypack | 9898 | 7743 |
uglifyParallel | 8764 |
已经做到了近 50% 的速度提升,相信随着项目体积的膨胀,速度提升占比还会再上升。