webpack 打包优化
最近项目不算忙,抽时间重构了一下项目的打包,先说一下成就。
在我的开发电脑上:
OS: macOS High Sierra
CPU: 2.6 GHz Intel Core i5
内存: 8G 1600 DDR3
硬盘: 1 TB SATA磁盘
代码全量编译时间从 4 分 51 秒优化到 2 分 08 - 20 秒左右。
在项目编译电脑上:
OS: Ubuntu 16.04.3 LTS
CPU: Intel® Core™ i5-7500 CPU @ 3.40GHz
内存: 64G 2133 DDR4
硬盘: 1 TB SSD
代码全量编译时间从 4 分 08 秒优化到 1 分 10 - 20 秒左右。
用了哪些手段
升级电脑
升级 SSD 可能是提升效果最明显的吧,从上面两组数据中就可以看出。相同的优化在 SSD 中表现要明显很多。
升级 webpack3
如果你的项目还在用 webpack2 的话,强烈建议你升级到 webpack3。webpack3 向下兼容,只不过有一些插件需要同时升级,注意看控制台给出的日志,把需要升级的一起升级了就好了。
Scope Hoisting-作用域提升
webpack 打包的时候,每个模块都被一个闭包函数包裹,过多的闭包函数降低了浏览器中 JS 执行效率,Scope Hoisting 的作用是减少闭包函数的数量,将有关联的模块放到同一个闭包函数中。
启用方法
1 | module.exports = { |
Scope Hoisting 是基于 ECMAScript Module syntax ,对于 Commonjs 和 AMD 的模块不适用。
上面升级的算是副本,下面才是正文。
正文开始
现在开发的项目算是比较大的项目,严格来说,是多个 SPA 组成的多项目。这样做的好处是能减少架构师的工作,同一份架构给多个项目使用,能保证项目稳定性。坏处也比较明显,就是会额外引入无用的依赖,比如共用的 helper 模块,很多项目都引用了,但是并不是每个项目都使用里面的每个函数。这点 tree-shaking 可以给出解决方案,但是实际开发过程中,由于同事们代码质量参差不齐,有些没用到的函数和模块也都引用了,所以导致 tree-shaking 的效果并不是很好。比如在大项目中,同事把几个 helper 里面函数全部封装到 vue-filter 中,当然里面的内容主要项目大多数都引用到了,但是后面同事在初始化一个小项目的同时,无论是否需要也都用了相同的代码(copy and paste)。
1 | import * as helpers from 'helpers' |
于是 helper 中每个 function 都挂载在 Vue-filter 中,所以完美的避开了 tree-shaking。
另外 tree-shaking 虽然能够一定程度的减少打包后代码的体积,但是开发和编译的速度还是会受到一定的影响。
下面是代码打包速度优化的一些思路,多数来源于网上的资料。
commonChunkPlugin 抽取公共代码
抽取公共代码有两个好处,一个是能减少编译代码的数量,一个是能够充分利用浏览器缓存,比如遇到项目切换的情况,使用 service-worker 中缓存共用的 common 代码能够减少请求的数量。
以下是 vue-cli 中给出的解决方案
1 | // split vendor js into its own file |
DLL 预编译
DLL 预编译的作用是将项目中稳定的依赖单独打包编译生成动态链接库,在业务代码中引用。这点在开发过程中优势比较明显,每次更新代码重新编译的时候都能够省去 DLL 库的编译,有不小的速度提升。
DLL 需要有一个额外的打包过程,新建一个 webpck.dll.conf.js 用来打包 DLL,并且在 package.json 中添加打包过程。
package.json
1 | { |
webpack.dll.conf.js
1 | const webpack = require('webpack') |
通过运行 npm run dll 在 dist 目录下生成了两个文件 vendor.dll.js 和 vendor.manifest.json。其中 vendor.dll.js 中是打包压缩后的 vendor 代码,vendor.manifest.json 是 vendor 文件的 node_modle 路径和 webpack 打包 id 的映射。
然后通过 DllReferencePlugin 将 vendor 引入业务代码。
1 | // 这里将生成的 vendor.dll.js 文件 copy 到 你需要的目录 |
最后还需要在 html 中引入生成的 DLL,网上有一些教程是直接把 script 标签写入 html 中的,但是由于我们多个项目同时依赖同一份 html 模板,其中某一些项目并不需要引入 DLL,比如一些静态页面。于是使用 html-webpack-include-assets-plugin 实现按需加载。
1 | ...pkgs.reduce((pre, current) => { |
在 pkgs 中控制 HtmlWebpackPlugin 的参数,和是否需要引入 vendor.dll.js。
pkg 模板如下:
1 | const extChunks = IS_PROD ? ['manifest'] : [] |
打包后可以明显看到 app.js 和 vendor.js 体积缩小,但是项目总体积略有增大。因为通过 DLL 的方式,额外存储了外部依赖的路径和 ID。
alias 减少搜索路径
这点想必大家都知道
1 | module.exports = { |
多线程加速
(1) uglifyjs-webpack-plugin 多线程提示 JS 压缩效率
使用 uglifyjs-webpack-plugin 不仅可以加速 webpack 压缩 js 代码的速度,还能与 webpack tree-shaking 配合,减少代码体积。webpack 本身并不会执行 tree-shaking。它需要依赖于像 UglifyJS 这样的第三方工具来执行实际的未引用代码(dead code)删除工作。
1 | new UglifyJsParallelPlugin({ |
记得开启缓存,能有效提升打包效率。
(2) happypack 多线程提升 loader 执行效率。
使用 happypack 之前,你可以先去 Loader Compatibility List 看一下 happypack 的兼容性列表。
1 | const os = require('os') |
使用 happypack 后,性能比较差的 mac mini 速度反而降低了一些,但是性能比较强的编译机速度有不少的提升,所以 happypack 可以酌情使用,测试后发现速度有提升再加入,没有提升就果断弃用。
缓存 HardSourceWebpackPlugin
hard-source-webpack-plugin 也是利用缓存效果提升打包速度。
HardSourceWebpackPlugin is a plugin for webpack to provide an intermediate caching step for modules.
用法很简单
1 | const HardSourceWebpackPlugin = require('hard-source-webpack-plugin') |
dev 优化
开发的时候,使用 koa-webpack-middleware 的 devMiddleware, hotMiddleware 两个中间件是提供 dev 服务和代码热新服务,devMiddleware 本质上是对 webpack-dev-middleware 的一层封装,而 hotMiddleware 是对 webpack-hot-middleware 的一层封装。
开发过程中,所有的代码均被载入两个 webpack 服务中,因此有一丁点的代码改动都需要重新编译所有的 buddle,这对开发过程是极其不好的体验,因此划分代码依赖,通过 npm 参数编译不同的项目,来达到加速开发的效果。
比如使用 npm run dev project1 来开发项目 project1,而其它代码并不加载到 webpack 中。
拿到 project1 参数可以通过 node.js 的 process 对象
1 | let projects = process.argv.slice(2) |
总结
以上打包优化都是参考网上的一些东西, 在实际使用过程中,发现有些文章内容是写了,但是并没有亲身实践,有些错误或者不完善的地方甚至都是一模一样的,所以自己结合实际项目走了一遍流程后,还是决定把东西写出来,希望对看到的人有帮助。