前言:最近重构一个项目(基于umi2脚手架搭建的),打包上线后发现包非常大,决定将项目优化一下,打包后的dist文件
可以看到打包后的dist文件有16M,然后部署上去发现首次打开蜗牛🐌般的速度,原因有一个公共依赖文件有7.6M之大,我giao,这怎么行呢!?
如果浏览器选项勾选了不允许缓存,那么将导致每次打开页面或者刷新都将会几乎如同首次加载一样,加载这些文件,每次都这么慢,致命致命致致命。
这在项目部署上线,用户访问时候是非常致命的,下面我们开始针对这个进行优化:
一、压缩静态资源
先从静态资源入手:图片。看下截图文件ued给的大小居然一张图片2M多,这...,索性自己来
使用这个网址: **https://tinypng.com/ **这个网站可以在保持清晰度变化不大的情况下把图片大小压缩,把稍微比较大的图片上传压缩, 可以看到,左侧是原图大小,右侧是压缩后的代码大小
把文件替换了,使用压缩后的图片。
二、使用打包分析工具---去除不需要的依赖
webpack的打包分析工具,我项目用的是umi2脚手架,所以直接根据文档配置即可,在.env文件新增:ANALYZE=1,然后umi build打包,会自动打开打包后的分析图。
Stat size :代表原始文件大小(文件没有经过任何处理的)
parsed size :解析后的文件大小,输出压缩过后的文件大小。(右键查看文件属性大小和这个大小一致)
gzipped:经过gizp压缩过后的代码大小(例如nginx可以开启gzip压缩),这里实际也是最终打开页面下载的文件大小是这个大。
把图上的包模块标注分析一下,红色的是目前以知的插件依赖,其他是引用的插件中有需要用到的插件:
首先分析下:echart、G6关系图谱、超图(地图)、代码编辑器这几个都是我项目中首页加载不到的(依据你的项目情况而定),我们需要把他从vendors.async.js文件中抽出来。这样就可以在打开首页时候(第一次加载),避免引入不需要的插件代码。
1、先从moment入手,时间插件上有很多国际语言,我们不需要用到,需要将它过滤掉:
在.umirc.js文件加入webpack,umi里面使用的是 chainWebpack
具体写法如下:
[](javascript:void(0); "复制代码")
<pre style="margin: 0px; padding: 0px; overflow: auto; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important; white-space: pre-wrap;"> chainWebpack(config) { //过滤掉momnet的那些不使用的国际化文件
config.plugin('replace').use(require('webpack').ContextReplacementPlugin).tap(() => { return [/moment[/\]locale$/, /zh-cn/];
});
},</pre>
](javascript:void(0); "复制代码")
接下来,umi build打包测试看效果moment时间依赖包体积明显了500k左右:
2、处理下echart,写法换成按需引入,打包时候也会按需打包
再次打包查看,dist文件大小,明显小了非常多(可能图片主要静态文件大小)!
我们发现即使这么做了,虽然把项目总体减小了,但是加载时候还有需要加载一个超级大的主文件,这就是一个问题, 之后我们应该用包分析模块显示的大小来做参考调整分割。进行第三步现在~~~
三、使用webpack分割代码---把主文件的依赖分离出来
以上步骤完成以后开始优化项目加载的具体操作,这里解释下为什么要这么做,因为我们通过分析发现没分割之前,浏览器打开页面时候下载的那个vendor.async.js文件非常大,并且里面的依赖不是我们每次都要用到,所以我们需要把这个文件拆分。
整理思路:拆分成什么样?我们需要把不是每次用到的依赖拆分出来,让他使用到的时候再根据那个页面按需要加载。
需要根据实际情况来定夺,实际目的只有一个,把主文件(里面的公共依赖文件用得比较少的)分割出来,多出引用的放在主文件里面(避免每次都要加载那部分的依赖代码文件),例如我项目中:
关系图谱只有一个页面有用到、echart只有一个页面用到、地图只有两个页面用到(一个前台一个后台),考虑到主文件太大因素,需要将这几个依赖拆分出来,等打开那几个特定页面时候按需要去加载这依赖代码。
开始拆分:使用webpck的 splitChunks
以下是webpack分割代码字段的解释说明:
[](javascript:void(0); "复制代码")
<pre style="margin: 0px; padding: 0px; overflow: auto; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important; white-space: pre-wrap;"> optimization: {
minimize: true,
splitChunks: {
chunks: 'async', //all: 不管文件是动态还是非动态载入,统一将文件分离。当页面首次载入会引入所有的包
//async: 将异步加载的文件分离,首次一般不引入,到需要异步引入的组件才会引入。
//initial:将异步和非异步的文件分离,如果一个文件被异步引入也被非异步引入,那它会被打包两次(注意和all区别),用于分离页面首次需要加载的包。
minSize: 30000,// 文件最小打包体积,单位byte,默认30000
minChunks: 1, //最少引入的次数 2:引入两次及以上被打包
automaticNameDelimiter: '.',// 打包分割符
cacheGroups: {
vendors: { // 项目基本框架等
chunks: 'all',
test: /(react|react-dom|react-dom-router|babel-polyfill|react-redux)/,
priority: 100,
name: 'vendors',
},
echartsVenodr: { // 异步加载echarts包
test: /echarts/,
priority: 100, // 高于async-commons优先级
name: 'echartsVenodr',
chunks: 'all',
minChunks: 5, //最少引入的次数 2:引入两次及以上被打包
}, },
},
},</pre>
](javascript:void(0); "复制代码")
<pre style="margin: 0px; padding: 0px; overflow: auto; white-space: pre-wrap; color: rgb(51, 51, 51); font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">chunks:有三个值:分别代表你想优化打包优化的类型,默认为async,截图看下这个意思
async:动态引入
</pre>
<pre style="margin: 0px; padding: 0px; overflow: auto; white-space: pre-wrap; color: rgb(51, 51, 51); font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">initial:直接引入加载</pre>
all:以上两者都进行打包分割优化
<pre style="margin: 0px; padding: 0px; overflow: auto; white-space: pre-wrap; color: rgb(51, 51, 51); font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">最终代码:</pre>
[](javascript:void(0); "复制代码")
<pre style="margin: 0px; padding: 0px; overflow: auto; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important; white-space: pre-wrap;"> chainWebpack(config) { //过滤掉momnet的那些不使用的国际化文件
config.plugin('replace').use(require('webpack').ContextReplacementPlugin).tap(() => { return [/moment[/\]locale$/, /zh-cn/];
});
config.merge({
optimization: {
minimize: true,
splitChunks: {
chunks: 'all', //all: 不管文件是动态还是非动态载入,统一将文件分离。当页面首次载入会引入所有的包
//async: 将异步加载的文件分离,首次一般不引入,到需要异步引入的组件才会引入。
//initial:将异步和非异步的文件分离,如果一个文件被异步引入也被非异步引入,那它会被打包两次(注意和all区别),用于分离页面首次需要加载的包。
minSize: 30000,// 文件最小打包体积,单位byte,默认30000
minChunks: 1, //最少引入的次数 2:引入两次及以上被打包
automaticNameDelimiter: '.',// 打包分割符
cacheGroups: { //优先级高于外面配置,理解为先分割后合并
vendors: { // 项目基本框架等
chunks: 'all',
test: /(react|react-dom|react-dom-router|babel-polyfill|react-redux)/,
priority: 100,
name: 'vendors',
},
echartsVenodr: { // 异步加载echarts包
test: /echarts/,
priority: 100, // 高于async-commons优先级
name: 'echartsVenodr',
chunks: 'async', // minChunks: 5, //最少引入的次数 2:引入两次及以上被打包
},
antvVenodr: { // 异步加载@antv包
test: /@antv/,
priority: 100, // 高于async-commons优先级
name: 'antvVenodr',
chunks: 'async',
}, 'async-commons': { // 异步加载公共包、组件等
chunks: 'async',
minChunks: 2,
name: 'async-commons',
priority: 90,
},
commons: { // 其他同步加载公共包
chunks: 'all',
minChunks: 2,
name: 'commons',
priority: 80,
},
},
},
},
});
},</pre>
](javascript:void(0); "复制代码")
实际效果:可以看到echart包、g6、codemirror这几个插件被单独抽离出来成js文件,不会再没用到情况下加载引入。
补充:在较早之前的版本,我们知道webpack可以将js文件打包压缩成js.gz文件,使得文件体积大大减小,但是现在大部分前后端分离的场景都是将前端项目部署在nginx,nginx上有个功能能将js压缩成gizp格式传输给浏览器,这也是为什么现在大都打包成js格式就可以,因为nginx上只要开启压缩功能,他会检测是否有.gz文件,没有的话会自动将js文件压缩传输。
总结:
1、首先通过webpack手段优化项目,(开启按需加载前提),只能对打包的代码分割,总体积上基本不可有很大的改变(某些情况可能会导致dist总体积变大,因为分离公共依赖包,能把公共的东西从vendors.async.js文件提取出来,同时也会把这个依赖拿出来在放到另一个或者另两个js文件里面做到按需加载,好处是能使得某个页面时候不需要一次引入这么大的一个文件。)
2、通过修改代码优化:例如将插件通过Index.html内的script标签方式引入(外网环境可以直接用CDN方式),这样可以使得公共依赖包减少体积。
3、简单粗暴图片压缩(webpack也有其他的图片转换压缩,例如将小于多少kb的图片转换为base64格式)
4、性能优化:多写PureComponent组件而不是普通的Component组件,因为pure有自动判断shouldComponentUpdate这个组件生命周期的功能(也只能进行浅比较:数组、对象类型,因为存储堆栈,引用地址指向问题还是会频繁更新)。备注:如果有写hook组件,那么使用hook的memo,usememo,usecallback,是最佳方案!