前言
回忆往昔岁月,我做前端开发至今已3年有余,做过的项目大部分都是业务需求迭代,技术优化经验相对较少。去年承蒙leader信任,给予机会开始做公司项目的技术优化。技术优化的首次任务,就是对整体前端项目进行性能优化(编译优化+构建体积优化)。
一、项目简介:
- 项目简介:我们团队主要做的是SaaS项目,有涉及B端的智能客服、在线客服、智能电销,C端聊天机器人等业务。
- 项目规模:产品:10人;前端:10人;后端:24人;测试:10人,和若干销售、运营人员等。
- 个人职责:我主要是负责私有化业务线的前端项目开发,然后就是在前端技术架构这块打打杂,做点项目优化的事情~~ 最近做项目复盘的时候,发现搞项目优化给我带来不少的收获和成长。今天我跟大家分享下做webpack性能优化的一段经历。
二、项目背景:
为什么要做webpack优化?
我司团队的项目使用的前端技术栈是vue + vuex + webpack
,由于业务代码过于庞杂,组件数量过多,而且引入的第三方依赖较多,因而出现了webpack编译速度较慢、编译打包后的代码体积过大、页面加载速度较慢等问题。
三、实践过程:
做webpack性能优化前,首先调研了其构建过程。了解到webpack启动后会根据entry配置的入口出发,递归遍历解析所依赖的文件,然后进行转换输出dist文件。简单来讲,webpack构建过程可以分为解析编译、打包输出的2个过程。因此,性能优化就从这2个方面着手。
编译优化
1、首先进行编译速度分析
我使用了 speed-measure-webpack-plugin 插件分析每个loader和plugin执行耗时具体情况。使用配置如下:
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp.wrap({
plugins: [new MyPlugin(), new MyOtherPlugin()],
});
由上图可知,编译总耗时81.56秒,其中vue-loader、css-loader、url-loader、babel-loader明显耗时较长。由此可以分析出webpack在文件搜索、解析时间过长,因为loader就是用来做文件解析转换的。
2、分析耗时情况,对症下药进行编译优化
文件搜索时间过久的优化措施
1)配置module.noParse,告诉webpack不必解析某些文件
比如 JQuery、Lodash 已经是可以直接运行在浏览器的文件,就不必再搜索解析。配置使用如下:
module.exports = {
//...
module: {
noParse: /jquery|lodash/,
},
};
2)配置loader,通过test、include、exclude缩小搜索范围,例如:
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
},
include: [resolve('src')],
exclude: /node_modules/,
}
]
}
}
文件解析过久的优化措施
1)解析loader开启多进程,使用thread-loader
thread-loader 使用起来也非常简单,只要把 thread-loader 放置在其他 loader 之前, 那 thread-loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行。
官方上说每个 worker 大概都要花费 600ms ,所以官方为了防止启动 worker 时的高延迟,提供了对 worker 池的优化。使用方式如下:
const threadLoader = require('thread-loader');
const jsWorkerPool = {
// 产生的 worker 的数量,默认是 (cpu 核心数 - 1)
// 当 require('os').cpus() 是 undefined 时,则为 1
workers: 2,
// 闲置时定时删除 worker 进程
// 默认为 500ms
// 可以设置为无穷大, 这样在监视模式(--watch)下可以保持 worker 持续存在
poolTimeout: 2000
};
threadLoader.warmup(jsWorkerPool, ['babel-loader']);
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'thread-loader',
options: jsWorkerPool
},
'babel-loader'
]
}
]
}
}
注意:如果小项目,文件不多,无需使用thread-loader开启多进程,这样反而打包会变慢,因为开启进程也是需要额外耗时的。
2)合理利用缓存
- loader启用缓存,比如使用loader本身的缓存或使用
cache-loader
- loader本身缓存
loader: 'babel-loader?cacheDirectory=ture'
- cache-loader 在一些性能开销较大的 loader 之前添加此 loader,以将结果缓存到磁盘里。
module.exports = { module: { rules: [ { test: /\.ext$/, use: [ 'cache-loader', ...loaders ], include: path.resolve('src') } ] } }
注意:保存和读取这些缓存文件会有一些时间开销,所以最好只对性能开销较大的 loader 使用loader缓存。
- 利用模块缓存提升二次构建的速度
使用 hard-source-webpack-plugin 为模块提供中间缓存,快速提升二次构建的速度。
module.exports = {
//...
plugin: [
new HardSourceWebpackPlugin({
cachePrune: {
maxAge: Infinity,
sizeThreshold: Infinity
}
})
]
}
代码压缩打包时间过久的优化方式
1)压缩代码插件开启多进程并行、缓存模式
- terser-webpack-plugin (webpack4推荐使用,支持es6语法)
const TerserJSPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new TerserJSPlugin({
cache: true,// 启用缓存
parallel: true// 开启多进程
})
],
},
};
如果你使用的是 webpack v5 或以上版本,你不需要安装这个插件。webpack v5 自带最新的 terser-webpack-plugin。如果使用 webpack v4,则必须安装 terser-webpack-plugin v4 的版本。
- uglifyjs-webpack-plugin(webpack3、4都可以使用)
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new UglifyJsPlugin({
cache: true,// 启用缓存
parallel: true// 开启多进程
})
],
},
};
采用上述优化手段,可以将总体编译速度提升了80%左右。我们项目优化后,编译时间从原来的81.56秒加快为13.268秒,编译速度提升了83%。
上述优化效果最明显的是 hard-source-webpack,虽然首次构建时间变化不大,但是第二次开始,编译构建速度提升了60-70%。
构建体积优化
1、对bundle体积大小进行分析
使用 webpack-bundle-analyzer 分析打包后生成Bundle的每个模块体积大小。配置如下:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
由上图可知,bundle文件里包含了很多较大的第三方依赖包、业务代码js/css、以及图片资源等。因此打包体积优化可以从这几个方面考虑优化。
2、因地制宜进行构建体积优化
bundle打包文件去除第三方依赖包
配置externals,防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。
module.exports = {
//...
externals: {
vue: 'Vue',
vuex: 'Vuex',
'vue-router': 'VueRouter',
axios: 'axios',
...
},
};
index.html再引入需要的第三方依赖js:
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.12/vue.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/vuex/3.0.1/vuex.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/vue-router/3.0.1/vue-router.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.19.0/axios.min.js"></script>
通过配置externals,大幅度减少了打包生成bundle的体积,因为配置的这些第三方依赖都没打包进来。 我司项目使用了该方法后,体积文件大幅度减少,index.js体积从4.34M减少到2.74M,变小了36%,如图所示:
Tree Shaking剔除无用的JavaScript
// package.json中添加
{
"sideEffects": ["*.css", "babel-polyfill"]
}
通过配置sideEffects,Tree Shaking便开启了,webpack打包时会自动剔除没有引用的js文件。对于业务文件冗余,但又不敢轻易删除的项目特别适合开启Tree Shaking,可以大幅度减少打包体积。
四、总结思考:
在做webpack优化过程中,我也遇到过一些问题。给大家总结一下,webpack优化主要需要注意以下几点:
- webpack配置要区分版本,webpack3.x和webpack4.x使用差异挺大的,需要参照官方文档来修改配置。没区分版本用一样的配置会报错哦~~
- 善于利用可视化工具进行分析,因地制宜制定优化方案。只有适合自己项目的优化方案才是最好的。 (荀子曰:假舆马者,非利足也,而致千里;假舟楫者,非能水也,而绝江河。君子生非异也,善假于物也。)
分析工具可参考:五种可视化方案分析 webpack 打包性能瓶颈
本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情