背景
前几天突然被拉去弄个紧急项目,然后就发现个现象,每次jenkins发版完后第一次打开都特别慢,如果在过去忙着实现功能可能都不会在意这种事情,多半就是接口之类响应慢吧,等等就好了。因为这次项目难度不大,很快就弄完了前端页面部分在等接口联调,闲着没事就开了个控制台:
好嘛,一个2.4M的js文件加载了快1分钟,平时在内网开发、测试都还感觉不到,碰上这次超级春节长假大家都远程办公,问题就体现出来了。
分析解决
在前端技术快速发展的这么些年,工程化程度已经很高了,通常要开发一个前端项目都无法避免的需要用到各种第三方库,而最终的发布包为防止出现请求数过多的连接损耗(据说http2已经不怕了),会将所有js打包合并打包成一个bundle,同时根据一定的策略(比如说拆封第三方包,跟自有逻辑代码),又会将bundle切分成多个文件,称为chunk,所以这里的问题就是最终打出来的bundle包太大了,事实上安装项目的逻辑复杂度不应该有这么多依赖才对。
针对第三方包切分chunk
分析问题的第一步就是需要知道,到底是哪个第三方包贡献了最大体积,这里可以通过修改webpack的配置来实现,以vue-cli3的配置为例:
1 | module.exports = { |
之后再次执行打包就能将各第三方包切分到不同的chunk文件中,结果如下:
1 | File Size Gzipped |
这样问题就很明显了,2个UI库antd跟element-ui合起来就超过了1.5M,平时因为偷懒对于这类UI库都是全包引入,即使对于antd就只使用了一个加载动画。
按需加载
一般来说对应的UI库都会提供按需加载方案,比如说andtd的,element-ui的,直接参考官网说明就好了,没有太多需要说明的,只是说为了方便管理,可以为不同UI组件库专门新建一个单独的js文件,将对于此UI库的组件引用都丢一块,然后在main.js中在引入这个文件就好了,比如:
1 | import Vue from 'vue' |
这样改造后的效果:
1 | File Size Gzipped |
可以说是效果相当明显了,antd直接就给压缩到了32k,于是就可以把分包策略再改一改了,把比较可能产生变化的element-ui单独拿出来作为一个chunk,其他第三方库作为一个统一的chunk,这样可以提供非首次加载用户的本地缓存命中率,只需要改动webpack配置的name方法逻辑
1 | name(module) { |
修改后打包得到的文件列表
1 | File Size Gzipped |
多入口配置
基于此,想起来之前另一个react工程也有类似的问题,也依葫芦画瓢分析一把,react的webpack配置需要安装对应的依赖包react-app-rewired,然后修改config-overrides.js文件
1 | module.exports = function override(config) { |
然后得到分包的输出结果
1 | File sizes after gzip: |
这个工程同时服务于PC端跟微信公众号,而在微信公众号上其实只有2个简单的页面,并不需要用到这么多的依赖,所以首先的优化点就是将微信的入口(entry)单独拆分出来,需要用到新的开发依赖react-app-rewire-multiple-entry,然后修改配置
1 | const multipleEntry = require('react-app-rewire-multiple-entry')([ |
最终打包得到的文件如下,可以发现多了很多数字开头的chunk,webpack会自动分析不同入口的依赖以对chunk进行切分
1 | File sizes after gzip: |
组件懒加载
通过入口的切分,微信端的问题是解决了,可PC端的问题依然存在,其中一个体积靠前的包是codemirror,一个富编辑器组件,但其实用到的只有2个页面,绝大部分页面并不需要用上,可是即使进行手工指定的chunk切分,也依然会在进入第一个页面的时候就进行加载,所以这里就需要用到react v16.6的新特性懒加载了
1 | import React, { lazy, Suspense } from 'react'; |
通过lazy-suspense的组合,就能够实现只有在当前页面被渲染时,才会第一次对依赖文件进行加载
1 | File sizes after gzip: |
以上是将组件修改为懒加载后的分包情况,可以发现,又多出了一些chunk
import()动态引入
最后要解决另一个体积比较大的包ol,这是一个地图工具包,本身不像UI组件库提供了按需引入的方式,也不像一个react的组件可以进行按需引入,所以就涉及到动态引入的概念了,这里是使用一个静态类对ol做了一层封装,最后改造如下:
1 | const MapEdit = (props: any) => { |
针对所有可以懒加载、动态引入的代码进行处理后再来看看打包情况
1 | File sizes after gzip: |
一下就多出来好几十个文件,来看看最终的加载效果,直接干掉了一个3M多的大包,虽然受制于带宽还是稍显有点儿慢,但比起过去等上1-2分钟已经是质的飞跃了。