背景
一直以来,性能都是技术层面不可避开的话题,尤其在中大型复杂项目中。犹如汽车整车性能,追求极速的同时,还要保障舒适性和实用性,而在汽车制造的每个环节、零件整合情况、发动机调校等等,都会最终影响用户体感以及商业达成,如下图性能对收益的影响。
性能优化是一个体系化、整体性的事情,印刻在项目开发环节的各个细节中,也是体现技术深度的大的战场。下面我将以Quick BI的复杂系统为背景,深扒整个性能优化的思路和手段,以及体系化的思考。
如何定位性能问题?
通常来讲,我们对动画的帧率是比较敏感的(16ms内),但如果出现性能问题,我们的实际体感可能就一个字:“慢”,但这并不能为我们解决问题提供任何帮助,由此我们需要剖析这个字背后的整条链路。
上图是浏览器通用的处理流程,结合我们的场景,我这里抽象成以下几个步骤:
可以看出,主要的耗时阶段分为两个:
阶段一:资源包下载(Download Code)
阶段二:执行 & 取数(Script Execution & Fetch Data)
如何深入这两个阶段,我们一般会用以下几个主要的工具来分析:
Network
首先我们要使用的一个工具是Chrome的Network,它能帮助我们初步定位瓶颈所在的环节:
如图示例,在Network中可以一目了然看到整个页面的:加载时间(Finish)、加载资源大小、请求数量、每个请求耗时及耗时点、资源优先级等等。上面示例可以很明显看出:整个页面加载的资源很大,接近了30MB。
Coverage(代码覆盖率)
对于复杂的前端工程,其工程构建的产物一般会存在冗余甚至未被使用的情况,这些无效加载的代码可以通过Coverage工具来实时分析:
如上图示例可以看到:整个页面28.3MB,其中19.5MB都未被使用(执行),其中engine-style.css文件的使用率只有不到0.7%
资源大图
刚才我们已经知道前端资源的利用率非常低,那么具体是哪些无效代码被引入进来了?这时候我们要借助webpack-bundle-analyzer来分析整个的构建产物(产物stats可以通过webpack --profile --json=stats.json输出):
如上例,结合我们当前业务可以看到构建产物的问题:
第一,初始包过大(common.js)
第二,存在多个重复包(momentjs等)
第三,依赖的第三方包体积过大
模块依赖关系
有了资源构建大图,我们也大概知道了可优化的点,但在一个系统中,成百上千的模块一般都是通过互相引用的方式组织在一起,打包工具再通过依赖关系将其构建在一起(比如打成common.js单个文件),想要直接移除掉某个模块代码或依赖可能并非易事,由此我们可能需要一定程度抽丝剥茧,借助工具理清系统中模块的依赖关系,再通过调整依赖或加载方式来作优化:
上图我们使用到的是webpack官方的analyse工具(其他工具还有:webpack-xray,Madge),只需要将资源大图stats.json上传即可得到整个依赖关系大图
Performance
前面讲到的都是和资源加载相关的工具,那么在分析 “执行 & 取数” 环节我们使用什么,Chrome提供了非常强大的工具:Performance:
如上图示例,我们可以至少发现几个点:主流程串化、长任务、高频任务。
如何优化性能?
结合刚才提到的分析工具,刚才提到的 “资源包下载”、“执行 & 取数” 两个大的阶段我们基本上已经覆盖到,其根本问题和解法也在不断的分析中逐步有了思路,这里我将结合我们这里的场景,给出一些不错的优化思路和效果
大包按需加载
要知道,前端工程构建打包(如webpack)一般是从entry出发,去寻找整棵依赖树(直接依赖),从而根据这棵树产出多个js和css文件bundle或trunk,而一个模块一旦出现在依赖树中,那么当页面加载entry的时候,同时也会加载该模块。
所以我们的思路是打破这种直接依赖,针对末端的模块改用异步依赖方式,如下:
将同步的import { Marker } from '@antv/l7'改为异步,这样在构建时,被依赖的Marker会形成一个chunk,仅在此段代码执行时(按需),该thunk才被加载,从而减少了首屏包的体积。
然而上面方案会存在一个问题,构建会将整个@antv/l7作为一个chunk,而非Marker部分代码,导致该chunk的TreeShaking失效,体积很大。我们可以使用构建分片方式解决:
如上,先创建Marker的分片文件,使之具备TreeShaking的能力,再在此基础上作异步引入。
下方是我们优化后的流程对比结果:
这一步,我们通过按需拆包,异步加载,节省了资源下载时间和部分执行时间
资源预加载
其实我们在分析阶段已经发现一个“主流程串化”的问题,js的执行是单线程,但浏览器实际上是多线程运行的,这里面就包括异步请求(fetch等),所以我们进一步的思路是把取数(Fetch Data)与资源下载通过多线程并行。
按照当前现状,接口取数的逻辑一般是耦合在业务逻辑或数据处理逻辑中的,所以解耦(与UI、业务模块等解耦)的步骤必不可少,将纯粹的fetch请求(及少量处理逻辑)剥离出来,放到优先级更高的阶段来发起请求。那么放到什么地方呢?我们知道,浏览器对资源的处理是有优先级的,正常按如下顺序:
HTML/CSS/FONT
Preload/SCRIPT/XHR
Image/Audio/Video