入职即巅峰?基建优化项目体积减少20%!
The following article is from 稀土掘金技术社区 Author 特立独行的猪
前言
故事背景是入职不久后被领导喊去优化基建相关的事,从上一篇面试篇知道,目前入职所从事的方向是底层代码的重构以及下一代产品的架构设计。入职不久后主管丢给我一个表格,上面列了四五条,都是当前项目存在的一些问题,让我看看有哪些可以去解决掉,一眼就瞅到了一个语言包优化,心想终于可以实践一下自己的优化知识储备了,当我准备大展拳脚时点开了webpack.config.js
,看到提示:xxxx, 4 years ago · 初始化
,.....,恍然大悟,原来我是铲💩官
。
问题大概是这样的,通过打包工具分析,同个语言包被打进每一个被依赖的模块中,导致多个模块传输体积增加。
关键是这个语言包体积挺大的,几个大文件我几乎占大头的都是它(为什么到现在才发现?💩)。出于安全问题,就不直接使用业务代码做例子了,我准备了一个类似的demo来还原当时的场景。牢骚发到这,开始我们的优化之路~
正文
代码仓库
https://github.com/my2061/sepPackages
使用的webpack为4.x版本,实现起来与5.x版本大同小异(5.x有些插件被内置,比如clean-webpack-plugin
),但不影响总体思路。
目录结构非常简单,两个业务模块中都用到了第三方模块jquery
和lodash
。但是如果不做优化处理的话那么最后的打包结果会出现两份jquery
和lodash
,随着业务模块的引入增多那么最终就会多出对应份的公共代码。
|-- node_modules
| |-- jquery
| |__ lodash
|-- src
| |-- handler.js //入口文件(业务代码),引入juqery和lodash
| |__ index.js //入口文件(业务代码),引入juqery和lodash
解决思路:利用分包提取大量公共代码,从而减少总体积并充分利用浏览器缓存。
先把环境安装好,使用webpack-bundle-analyzer
来进行性能分析,两个入口文件,简单配置下webpack.config.js
//webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
entry: {
main: "./src/index.js", //入口文件
handler: "./src/handler.js", //其他入口文件
},
output: {
filename: "[name].[hash:4].js", //打包后的文件名用chunk name和hash来组合命名
},
plugins: [
new BundleAnalyzerPlugin(), //注册打包分析plugins
]
}
不优化正常打包
使用npm run prod
进行生产环境打包
厚礼谢,可以看到我们的业务代码里并没有写什么东西,但是两个文件里都包含了包含了jquery
和lodash
的代码,所以导致两个文件都达到了160kb
的体积。这只是两个文件,如果在一个复杂的大型系统里,不知道会有多少公共依赖,会增加多少传输量。
优化后打包
从打包分析中可以看到两个文件体积明显减少,解析后仅有1.1kb
,依赖的jquery
和lodash
都变成了dll-reference
表示引用,再看network
,可以发现两个库都被单独的抽离了出来。
优化前:main(160kb)+ handler(160kb) = 320kb
优化后:main + handler(2kb)+ jquery(91kb) + lodash(74kb) = 167kb
优化后体积缩小了接近50%
,斯国一!(杠精:垃圾,多增加了两次传输!!呵呵,分包另一个好处就是因为第三方库不会频繁变动文件内容,所以可以充分利用浏览器缓存~) 接下来就开始精解两种分包的原理和实践。
手动分包
原理
顾名思义,就是要先手动的将公共文件先单独打包出来,成为动态链接库dll(Dynamic Link Library)
,生成一个资源清单(manifest.json)
。至于什么是dll
还是在上学时接触.net网站开发了解的,在分包中简单的理解为一个代码仓库
,你要哪些东西直接去里面拿,没了解过的同学自行百度哈,这里就不过多解释了。
怎么做?该如何配置?在实践中会详细讲解,有了动态链接库
和资源清单
后,我们就可以正常进行打包了,在正常打包的过程中,如果发现导入的路径
跟资源清单
中记录的模块名称相同,那么就会使用动态链接库
中的文件,就不会将依赖打包进自己的文件中。
这里有一个小问题,这里为什么打包出来的jquery
和lodash
要直接使用var
暴露全局变量出来?
我们直接来分析下webpack
的打包结果就一目了然了,使用mode=dev
开发模式打包,看的更清楚一些。
// dist/dll/main.6321.js
// ....其他代码....
"dll-reference jquery":
(function (module, exports) {
eval("module.exports = jquery;\n\n//# sourceURL=webpack:///external_%22jquery%22?");
}),
"dll-reference lodash":
(function (module, exports) {
eval("module.exports = lodash;\n\n//# sourceURL=webpack:///external_%22lodash%22?");
})
可以发现并没有将jquery
和lodash
一大堆源码打入到main.js
中。而是使用了module.exports
的方式导出一个全局变量。
为什么? 因为在前面生成的资源清单中有关于jquery或lodash的描述
。打包过程中分析依赖凡是看到依赖的名称为jquery
的,都会去资源清单的content
的路径进行匹配,也就是./node_modules/jquery/dist/jquery.js
。
// 我们平时写的引入路径
import $ from "jquery" === import $ from "node_modules/jquery/dist/jquery.js"
node_modules/jquery/dist/jquery.js
正好匹配到了jquery.manifest.json
资源清单文件的content
路径,所以在打包结果中jquery
的源码变成了:module.exports = 该资源清单的name
。
// jquery清单文件
// jquery.manifest.json
{
"name": "jquery",
"content": {
"./node_modules/jquery/dist/jquery.js": {
"id": 1,
"buildMeta": {
"providedExports": true
}
}
}
}
之后直接在模版index.html
中直接引入公共代码dist/dll/jquery.js
就可以完成手动分包的目的。有些懵?没事,走一遍实践就明白了。
实践
前面提到的资源清单
和动态链接库
如何生成呢?新建webpack.dll.config.js
,配置如下:
// webpack.dll.config.js
const webpack = require("webpack");
const path = require("path");
module.exports = {
entry: {
//有几个公共文件就写几个入口
jquery: ["jquery"],
lodash: ["lodash"]
},
output: {
//打包到dist/dll目录下
filename: "dll/[name].js",
library: "[name]"
},
plugins: [
new webpack.DllPlugin({
//资源清单的保存位置
path: path.resolve(__dirname, "dll", "[name].manifest.json"),
//资源清单中,暴露的变量名
name: "[name]"
}),
]
};
然后在packages.json
中添加script
脚本dll: "webpack --mode=production --config webpack.dll.config.js"
指明配置文件。运行npm run dll
。
可以看到我们想要的文件都被打包出来了,用来描述清单的manifest
文件。以及打包结果中的dll
文件夹。我们在来看一下打包后的jquery.js
和lodash.js
是不是导出了一个全局变量。
哦♂ yeah~ 我们的配置生效了,公共代码分出来了,前置工作已经做好了,接下来就可以正常进行打包了。进一步完善webpack.config.js
。
这里安装两个新插件html-webpack-plugin
和clean-webpack-plugin
(5.x版本已内置),在plugins
中新增两个webpack.DllReferencePlugin,配置一下manifest
指明清单文件。
//webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const webpack = require("webpack");
module.exports = {
mode: "production",
entry: {
main: "./src/index.js",
handler: "./src/handler.js",
},
output: {
filename: "[name].[hash:4].js"
},
plugins: [
// 打包分析工具
new BundleAnalyzerPlugin(),
new CleanWebpackPlugin({
// 不删除手动打包出来dll文件,这样每次打包就不会删除dist/dll文件了
cleanOnceBeforeBuildPatterns: ["**/*", "!dll", "!dll/*"],
}),
new HtmlWebpackPlugin({
template: "./index.html"
}),
// 指明清单文件
new webpack.DllReferencePlugin({
manifest: require("./dll/jquery.manifest.json"),
}),
// 指明清单文件
new webpack.DllReferencePlugin({
manifest: require("./dll/lodash.manifest.json")
})
]
}
然后在模版文件index.html
中手动引入dist/dll
下的公共代码。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webpack sep packages</title>
</head>
<body>
<script src="./dll/jquery.js"></script>
<script src="./dll/lodash.js"></script>
</body>
</html>
然后敲下npm run prod
,可以看到打包分析工具启动端口打开页面,呈现在我们眼前的就是文章前面提到的优化后打包
的样子啦。至此,手动分包
的原理及使用就全部ok了。但是有没有觉得有一丝繁琐
?还有一个问题就是如果第三方库依赖其他的第三方库,那我是不是要先把依赖的依赖先打包,然后再打包依赖?这些就是手动分包
的一些缺点,如何解决?先看看自动分包
怎么做。
自动分包
原理
与手动分包
不同的是,自动分包
无需每次都要手动先将公共代码先打包一次,它不针对某个具体的包分出去,我们只需要配置好分包策略
后webpack
每次都会自动的完成分包的流程,更符合我们的开发方式,无需关注以后会新增哪些公共代码,所以我们一般优化基本上都用的是自动分包
,一次配置,永久畅享~
事实上,webpack
内部完成分包依赖的是SplitChunksPlugin来实现的,可以在官方文档中看见过去是使用CommonsChunkPlugin
来实现的分包,从v4
版本后就换成了SplitChunksPlugin
。所以自动分包的策略实际上是对配置文件webpack.config.js
中optimization.splitChunks
配置项的修改。
从图中可以看到,经过分包策略后webpack
开启了一个新的chunk
,对公共代码进行打包,并且在输出文件的时被提取出来形成了一个单独的文件,它是新chunk
打包出来的产物。
最后在公共代码
打包出来的文件内挂载一个全局变量window.webpackJsonp = [common1、common2、...]
,然后使用到公共代码
的chunk
从这个数组中拿,今后有再分出来的包继续添加到数组中。最后把打包出去的模块
从原始包中删除,并且修正原始包的代码。
实践
了解到大概原理其实根据分包策略开启若干个新chunk
并打包形成一个单独的文件,并且挂载一个全局变量webpackJsonp
来存放公共代码。如何配置?最优解是什么?那么就进入到实践环节。
基本配置
这里就不新开一个单独的项目来演示了,在原有项目中新建webpack.auto.config.js
专门用来做自动分包
(正常项目中直接在默认的config
文件中配置就行,不用专门新建文件)。
//webpack.auto.config.js
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin")
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
mode: "production",
entry: {
main: "./src/index.js",
handler: "./src/handler.js",
},
output: {
filename: "[name].[hash:4].js"
},
optimization: {
splitChunks: {
//优化配置项...
}
},
plugins: [
new BundleAnalyzerPlugin(),
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template:"./index.html",
})
],
};
除了最基本的配置外,主要就是对webpack
提供optimization
的优化配置项中的splitChunks进行修改。那么可以写哪些配置呢,我单独列了自动分包可能用到的配置项,想要拓展的朋友可以去英文文档上自行翻阅。
名称 | 作用 | 默认值 |
---|---|---|
chunks | 哪些chunk 需要应用分包策略 | async(异步chunk) |
maxSize | 分出来的包超过 了多少字节需要继续进行拆分 | 0 |
minChunks | 模块被chunk 引用了多少次才会进行分包 | 1 |
minSize | 模块超过多少字节才会进行拆分 | 20000 |
automaticNameDelimiter | 公共代码的新chunk 名称的分隔符 | ~ |
1. chunks
chunks
有三个取值,默认值为async
,表示只对异步的chunk
开启分包,就是懒加载
的模块。所以如果没有懒加载
的模块并不配置的话直接打包是看不什么效果的,所以一般使用all
来表示对所有的chunk
都要应用分包策略。
optimization: {
splitChunks: {
//优化配置项...
chunks: "all",
}
},
2. maxSize
maxSize
表示如果某个包(包括经过分包后的包)体积大于了设定的值,那么webpack
会尽最大努力继续再次分离,什么意思呢?比如开启了chunks: "all"
后的打包结果是这样的。
可以看到开启了all
后,jquery
和lodash
被合并打包成了一个新的文件,而不是像手动分包
那样将每个源码都映射一份出来。那么这个新的文件vendors~handler~main.2fca.js
是公共代码对应的新chunk
的产物。但是如果觉得160kb
还是有点大,能不能再继续分一分?这就要用到maxSize
配置项了。
optimization: {
splitChunks: {
//优化配置项...
chunks: "all",
maxSize: 50 * 1024,
}
},
表示如果包大于了50kb
那么webpack
还会去进一步的继续尝试着分离包。运行npm run auto
。
能发现比刚刚多了一个文件,都是vendors
开头的。点开进去之后可以看到分别是jquery
和lodash
的源码。同上所说,打包出来的文件都去定义一个全局变量webpackJsonp
,那如果存在多个文件不就覆盖了吗?
其实不然,真正每个verdors
文件第一行是(window.webpackJsonp = window.webpackJsonp || []).push(序号索引)
,按照分出来的顺序,序号索引就是0、1、...,这样就无论分几个包,最后其实还都是会push
到同一个window.webpackJsonp
数组中。
细心的同学应该会发现一个问题就是明明设置的是50kb
呀,为什么拆分出来的两个文件都还是大于设置的值呢?仔细看前面说的如果超过了设定值,webpack
会尽最大努力
继续分离,但是,是以模块
为基础的,再怎么分,都不可能把原来的一个整体的模块
直接打乱掉代码分割出去,最小的单元
就是一个模块
。所以最小也只能jquery
和lodash
完整的代码再单独各分出去。
走到这大家就会发现有点不对劲,再怎么分总体积是不变的,只不过是拆分了很多份,有些时候反而对性能是负提升
。但是如果某个浏览器
支持多线程请求的话,可能会对性能有帮助。大家对这个属性仁者见仁,智者见智就好,谨慎使用
。
3. minChunks
表示一个chunk
被引用了多少次,才会进行分包。默认值是1
,只要被引用过就要进行分包优化,这个配置就很简单,可以试一试设置为一个大点的值,那么就不会进行分包了。
optimization: {
splitChunks: {
//优化配置项...
chunks: "all",
maxSize: 60 * 1024,
minChunks: 20,
}
},
注意: 这里设置的minChunks
是针对于我们自己写的一些公共模块
想要进行分包处理的最小引用数,针对于引用依赖node_modules
中的文件是不生效
的,因为存在缓存组
单独针对node_modules
的规则,下面会说到,所以这个配置对第三方库是不生效
的!
4. minSize
此配置表示当一个包
达到多大体积才会进行分包,默认值为20000
。对第三方库
同样生效。如果设置一个很大的值,那么就都不会进行分包。
optimization: {
splitChunks: {
//优化配置项...
chunks: "all",
maxSize: 60 * 1024,
minChunks: 20,
minSize: 1000000,
}
},
5. automaticNameDelimiter
这个配置项十分简单,只是修改新chunk
生成的文件名中的分隔符,默认值~
。比如现在改成---
,那么新chunk
的文件名分隔符就是vendors---handler---main.b457.js
。
optimization: {
splitChunks: {
//优化配置项...
chunks: "all",
// maxSize: 60 * 1024,
// minChunks: 20,
minSize: 20000,
automaticNameDelimiter: "---"
}
},
缓存组
前面提到了minChunks
配置对第三方库不生效,是因为有缓存组
的存在,那么缓存组
到底是什么?在之前设置的配置都是基于全局的,实际上,分包策略
是基于缓存组
的,每一个缓存组
都是一套单独的分包策略
,可以设置不同的缓存组
来针对不同的文件进行分包
。webpack
默认开启了两个缓存组
,即cacheGroups。
optimization: {
splitChunks: {
//优化配置项...
chunks: "all",
cacheGroups: {
vendors: { //属性名即是缓存的名称,改成common看看
test: /[\\/]node_modules[\\/]/,
priority: 2
},
default: { //默认缓存组设置,会覆盖掉全局设置
minChunks: 2,
priority: 1,
reuseExistingChunk: true
}
}
}
},
在cacheGroups
中,每一个对象就是一个缓存组
分包策略,属性名
便是缓存组
的名称,对象中设置的值可以继承
全局设置的属性(如minChunks
等),也存在只有缓存组
独特的属性,比如test
(匹配模块名称规则)、priority
(缓存组的优先级),reuseExistingChunk
(重用已经被分离出的chunk)。
webpack
会根据缓存组
的优先级(priority)来依次处理每个缓存组
,被缓存组
处理过的模块不需要再次分包。所以前面为什么minChunks
对第三方库没有生效,是因为有默认缓存组
的存在,已经针对node_modules
定了一套独有的分包策略。
可以把默认的vendors
的属性名改成其他的,比如common
,那么打包结果中,经过缓存组
处理过的node_modules
分出来的包就是以common
开头了。
正常情况下,缓存组
对我们来说没有太多意义,webpack
提供的默认缓存组
就已经够用了。但是大家可以想一想,它其实还可以用来做对公共样式
的抽离,比如两个css
文件有相同的样式,那么我们可以用test
匹配css
文件来设置公共样式
的打包。这个就属于拓展了,有兴趣的同学可以试一试找我交流~
总结
两者分包的区别在于手动分包
可以极大的提升编译构建速度,但是使用起来比较繁琐
,一旦今后有新增的公共代码
都需要手动去处理。自动分包
的话可以极大的提高开发效率,只要配置好分包策略
后就一劳永逸了。深入了解两者分包
的原理及优缺点,还有一些比较冷门的点后,就已经基本拿捏了,斯国一!遇到相同的业务场景时,可以选择合适的分包手段来进行优化。
但是分包也是有局限性
的,比如已经分到不能再分的时候,就只能通过代码压缩
、tree shaking
、懒加载
等等手段来继续优化了,本文就不过多赘述了,以后可以出个续集。最后,谢谢大家的观看,对本文内容有异议或交流欢迎评论~
参考资料
https://juejin.cn/post/7112770927082864653: https://juejin.cn/post/7112770927082864653