用Performance面板做前端性能优化让我上瘾!

一、背景

关于Performance面板的基础用法介绍,可参考上一篇文章《"Performance面板"一文通,解锁前端性能优化工具基础用法!》。文章中还从一个HTTP请求的四阶段的角度来介绍Performance图的"观看方式",并重点介绍了worker线程跟主线程的协作关系

本篇文章中,我们将会以一个实际网页 ------VPC列表页为例,介绍Performance抓图及分析的过程,并将上一篇文章中介绍的相关内容串起来,希望每位frontend developer都能掌握用Performance分析页面性能的能力。

PS:这里假设我们的分析目标是:让列表页的主内容区域尽快展示出来,以避免长时间白屏;另外,为求简洁,下文叙述中统一将Performance录制的结果图叫做"性能图"

二、分析前准备

正式分析之前,建议做一些准备工作,能有效提高后续的分析效率。

1. 环境准备

1)选一个良辰吉日

建议关闭一些应用程序、浏览器页签,尽量让电脑CPU空闲一些 ,(也建议关掉通信软件,让自己心情好一些)。尽量避免一边开会一边分析,避免分析过程被频繁打断,因为分析过程可能遇到各种奇怪的现象,倘若再被其它事务打断,心态直接崩溃,那么就分析不下去了......

2)选浏览器无痕模式

建议在浏览器无痕模式下录制性能图并进行分析,因为一些特殊的浏览器配置、浏览器插件 都可能影响网页的加载过程,干扰分析结果。

3)选生产环境进行分析

我们可能有多种不同环境:本地代理环境(俗称localhost)、类生产环境、生产环境 等。只有生产环境是最真实贴近用户的,所以建议在生产环境下进行网页性能分析。

但需要注意的是,生产环境部署的代码,跟源码往往有较大形态上的差异(eg:打包工程的混淆、部署时的加工、服务端渲染的处理等)。所以,生产环境的性能图在某些代码细节上可能无法深入研究,此刻,我们还是需要其它环境的性能图进行辅助对比。

2. 代码准备

1)了解代码结构和加载流程

建议先从源码角度,详细了解待分析页面的结构及加载流程,例如我们讨论的样例------ VPC列表页的结构如下:

页面结构

  • 红色框:由基础框架console-ui提供的header、sidebar 实现
  • 蓝色框:由业务代码实现,采用angular技术框架,NG App下挂载一个根组件(VpcComponent)
  • 绿色框:根组件下分为 导航组件(LeftmenuComponent)和 列表页组件(VpcListComponent)

代码结构

html 复制代码
<!-- index.html -->

<div id="header"><!-- console-ui负责渲染 --></div>

<div id="sidebar"><!-- console-ui负责渲染 --></div>

<!-- NG App 挂载点 -->

<div id="ngApp">

    <!-- 根组件 -->

    <vpc-component>

        <leftmenu-component><!-- 导航组件 --></leftmenu-component>

        <router-outlet>

            <!-- 内容区-路由渲染点 -->

            <vpc-list-component><!-- 列表页组件 --></vpc-list-component>

        </router-outlet>

    </vpc-component>

</div>
  • console-ui和NG App并行工作,分别负责不同div的渲染
  • APP的根组件下,导航组件和内容区并行工作,内容区由路由加载列表组件

初始化流程

  • 主渲染流程是:NG App启动 => 加载根组件 => 路由渲染 => 加载列表页组件
  • angular中每个组件都会经历生命周期:constructor => ... => ngOnInit => ... => ngAfterViewInit => ... (这里只列举一些常用生命周期钩子,完整的说明见 组件生命周期PS:这一点不理解也不妨碍下文阅读

2)关键节点加performance.mark

很关键的是:在我们的主渲染流程上一些关键的时间节点加上performance.mark,或者关键时间段加上console.time/timeEnd。这样在性能图的Timings面板上就会显示这些标记,从而帮助我们确认各个执行环节。

例如,针对VPC列表页的主渲染流程,我们在 NG App启动、根组件constructor及ngAfterViewInit、列表页组件constructor及ngAfterViewInit,分别加上performance.mark,则能在Timings面板中看到下图情况:

PS1:Timings面板中的线很细,不仔细观察可能会漏掉。

PS2:为了针对生产环境进行分析,我们可能需要提前在代码中预埋performance.mark,待上线后才能利用的上。

3)录制性能图

在Network面板清空已有记录,在Performance面板多点几下垃圾回收,然后开始录制。录制完成后,将Performance面板的性能图导出成json文件,并将Network面板的网路请求记录导出成har文件,保存在本地以方便后续查看。

三、数据分析

录制好性能图后,建议先分析Network面板中的index.html,大致了解加载了哪些静态资源,然后再投入性能图的分析

1. 分析index.html

首先分析下Network面板中的index.html,大致了解页面加载了哪些静态资源。 一般源码中的index.html跟浏览器实际执行的index.html往往有很大差别,因为代码(打包)工程会对index.html进行魔改,如果有服务端渲染机制,那么服务渲染时可能还会插入一些样式、脚本。所以,浏览器最终执行的index.html将会是 源码+工程修改+服务端渲染 之后的结果。

例如,针对VPC列表页的html进行分析,会发现其加载了以下资源(按html中从上到下的顺序排列)

我们需要搞清楚浏览器会开几个TCP连接 ?哪些资源会挤在同一个连接中?因为同一个连接中的资源会互相争抢网络带宽

分辨方法是:h2下同一个域名的资源会共用同一个TCP连接,http/1.1下同一个域名可能会开多个TCP连接,资源们按顺序排队。 所以上表中,所有CDN域下的资源共用同一个TCP连接,Server域下只有一个资源,暂时只开一个TCP连接。

PS:说"暂时"是因为,后续可能会有动态加载js、或者发起ajax、fetch请求,可能会增加TCP连接数量

另外,下载优先级是由浏览器综合分析并自动分配的,我们无法直接指定优先级。并且不同版本浏览器的分配策略各异,但大多数情况下,会遵循如下规则:

  • 通过<script>标签加载的js脚本的优先级高于动态创建script的优先级。(动态创建例如:通过appendChild往DOM中插入一个<script>标签)

  • <script>标签上没有加任何标记(module、async、defer)的,优先级最高

  • <script>标签被标记了type="module"的,优先级较高

  • <script>标签被标记了async、defer的,优先级较低

值得说明的是,项目采用了webpack打包,其中main.{hash}.js就是webpack主入口文件,而vendor~tinycloud等文件均是分包策略拆出来的chunks们。

2. 分析Performance数据

接着,我们就要分析性能图了,我们的首要目标是:搞清楚性能图中各个环节在做什么,并将它跟初始化流程一一对应起来

2.1 分析映射关系:代码 <=> 性能图

首先观察性能图中 Network(网络情况)、Main(CPU情况)、Timings(我们预埋的mark标记) 这三个部分,尝试搞清楚图中每个时间段里面,浏览器在忙些什么。 以上图为例,大致流程是:

  • 时段1:网络繁忙、CPU空闲
  • 时段2:网络空闲、CPU繁忙
  • 时段3:网络CPU都繁忙
  • NG App启动从时段3才开始

接下就是详细分析过程:

1)时段1:静态资源下载

分析每一个请求,搞清楚它是谁发起的,在业务上有什么作用。 点开一个资源条,就可以在Summary面板中看到它的一些基础信息。

PS:有时候,Summary面板中写的Initiated by不一定是真实的发起者,真实发起者可能被层层代码封装隐藏了,我们需要到Network面板中的Initiator里面去找真实发起者

对时段1的所有请求进行分析之后,我们就搞清楚了这个阶段的具体情况,如下:

  • 绿色部分由console-ui发起

    • 首轮请求(html中通过script直接引用):仅consoleui.umd.js这1个资源 (CDN域/h2)
    • 非首轮请求(由某些逻辑动态发起):5个静态资源 (CDN域/h2) ;若干Fetch/XHR请求 (Server域/http1.1)
  • 红色部分由业务代码发起

    • 首轮请求(html中通过script直接引用):runtime、polyfill、......、main等11个资源 (CDN域/h2)
    • 非首轮请求(由某些逻辑动态发起):无
  • 蓝色部分由cc组件(一个三方业务组件,负责一些特殊业务组件的实现)发起

    • 首轮请求(html中通过script直接引用):无
    • 非首轮请求(由某些逻辑动态发起):cc-main.js (Server域/http1.1) 及若干main、theme等 (CDN域/h2)

时段小结: 这一时段主要是完成各种HTTP请求,主要有 业务代码、console-ui、cc组件三方参与。首轮请求主要是业务代码的请求,采用h2协议,console-ui及cc组件则多为非首轮请求。

这里其实可以看出来,业务代码发起的静态资源请求几乎独占首轮请求,其它请求均是在后续轮发起,不会影响业务静态资源请求的完成。并且所有Fetch/XHR请求都是使用 Server域/http1.1,跟静态资源使用的 CDN域/h2是两个不同的TCP通道,不会影响业务静态资源的加载。

2)时段2:webpack代码展开

分析火焰图中每个色块,搞清楚它们是属于哪个代码文件的内容,它们在执行什么?

上一篇文章中介绍过,每个色块是一个函数,色块的名字是函数名,色块上下关系是函数调用关系,这一整个火焰图就是调用栈的直观展示。

  • 查看色块归属:在上一篇文章中提到过,webpack打包时会将js代码用匿名函数包裹,并指定一个数字key,所以就产生了这些数字命名的函数。点击色块可以看到它属于哪个代码文件。
  • 查看色块在做什么:查看这个task的火焰图的栈底,会发现全是黄色的Compile code块,说明在编译代码(V8引擎的惰性编译策略)
  • 整个展开动作的起点:等待初始chunk全都下载完之后,才开始代码展开。

时段小结:这一时段主要在做webpack的代码展开,CPU在忙着编译、执行被webpack打包的代码

这里需要对webpack打包产物有一定了解,才能透彻了解这个过程,简要说明如下:

标题中所谓的webpack代码展开,其实就是执行这些数字命名的包裹函数,而V8引擎的惰性编译策略,可能不会在流式下载文件的环节就直接编译这些代码,所以栈底全都是compile code的色块。另外,webpack的启动机制会保证所有chunk下载完成后,才启动代码展开工作,从 时段1 => 时段2 的衔接点可以看到,虽然主chunk文件main.xxxx.js早已下载完成,但代码没有立即展开,而是等待最后一个chunk下载完成之后,才进行展开。

3)时段3:动态下载语言包等主题资源

利用时段1提到的分析方法,对时段3的请求也做同样的分析可知:

  • 主要是语言包、主题资源包、docs等通用资源的下载,主要使用 CDN域/h2 的通道。
  • 均是由业务代码发起。

利用时段2提到的分析方法,对时段3的火焰图的各个色块也进行同样的分析可知:

  • 一些由微任务或定时器,唤起的console-ui逻辑被执行
  • 一些cc组件逻辑被执行
  • 还存在着若干空闲task时间

同时,从Timings面板可以看出,在webpack代码展开之后、时段3开始之前,NG App就已经boot了,但是在时段3结束后,根组件才开始construct。

为什么angular的app已经开始boot了,但根组件没有尽快construct?并且这中间还有空闲的CPU时间。 这里需要结合代码实现来分析:

  • 该页面支持多语言,为了不一窝蜂的下载所有语种的资源包,代码中采用按需下载的模式:仅在确认了当前语种之后,才开始下载该语种的资源包。
  • 代码中利用angular router的路由守卫,在守卫中异步下载当前语种对应的资源包。angular router会确保根组件在守卫完成后再开始construct。

所以,问题的答案是:根组件在等待资源包的下载完成。从图中也可以看出,根组件construct是在docs接口完成之后的第1个task中进行的。

时段小结: 这一时段主要是下载当前语种对应的资源包,而这些资源包是根组件construct的前提条件(因为路由守卫的原因) 。

而在资源下载期间,CPU被用于执行一些console-ui、cc组件等非业务逻辑,或者直接空闲。因为在这期间,也没有业务逻辑代码可供执行了。

4)时段4:关于时段3之后

从Timings面板中可知,在时段3之后、特别是根组件construct之后,根组件afterViewInit、列表组件的construct和afterViewInit都紧锣密鼓的执行起来了。这一段CPU极其繁忙,按生命周期顺序执行各个组件的初始化,直到列表组件的afterViewInit完成后,列表页主内容区域才展示出来。

针对这种任务密集时段,我们当然也可以用时段2中提到的分析方法,针对火焰图中各个色块进行分析。但从生产环境录制的性能图中看到的色块名(函数名)可能都是混淆之后的结果,不利于跟源码对应起来。此时,我们可以针对本地调试环境(localhost)录制性能图并进行分析,因为代码执行在火焰图上的表现,往往不会受到环境的影响。例如该时段中有这么一段:

可以看出有一段结构很相似的调用,出现了多次重复。从localhost环境抓取的性能图中就可以明显看出,这段重复执行的是 detectChangesInEmbeddedViews 函数,它是angular的内部函数。在嵌入式视图场景中,如果有大量ngFor、ngIf就会触发。通过源码分析,这一段正是导航组件中的代码实现造成的。

2.2 在性能图中找问题

通过上一节中对性能图透彻分析之后,我们已经能将性能图跟源码对应起来,并且能将性能图跟加载流程对应起来,同时对一些关键节点心中有数。接下来要做的就是找问题、找可优化的空间,以达到我们的目标。例如针对VPC列表页的分析可知,主渲染流程在性能图中大致是:

为了实现目标,我们至少可以找到以下优化点:

  • 下载静态资源能否更快?

    • 非重要模块,改为懒加载(按需动态import),减少初始chunk的体积
    • 较大的图片资源等,避免打包成base64字符串,减少chunk体积
    • 合理拆分chunk包,避免有一个独大的chunk,充分利用h2的并行下载效果
  • webpack代码展开能否更快?

    • 减少初始chunk的体积。需要展开的代码少了,展开的自然更快
    • 避免在代码中执行长耗时运算等
  • 下载语言包资源能否更快?

    • 语言包资源提前下载,和前面的静态资源一起下载。
  • 组件生命周期能否执行的更快?

    • 削减高频重复执行的detectChangesInEmbeddedViews函数的执行次数。
    • 减少非必要的渲染内容。

总体上看,不同的项目会有不同的性能图和性能瓶颈点,通用的优化方案可以解决一些常规问题,但当所有常规方案做完之后效果还是不够满意时,我们可能得进行针对性的分析来查找问题。通过深入性能图分析,搞清楚 代码<=>性能图 之间的映射关系之后,我们就能轻松找到性能瓶颈点,从而找对应的解决方案了。

四、总结

通过Performance面板录制页面加载性能图并进行性能分析,是每一个frontend developer进阶的必备技能之一。性能图分析除了要求我们掌握Performance面板的基本用法之外,还要求我们对前端相关知识例如:webpack工程打包、浏览器的加载运行、HTTP协议机制、前端框架的原理、等都有一定了解,同时要求我们对项目代码的结构和执行流程足够清晰明确。常规的优化方案往往只能解决一些初级、普遍的问题,但每个页面有每个页面的具体情况,只有对页面进行充分分析之后,才能搞清楚页面的性能优化点在哪里,从而有条不紊的落地实施。

关于 OpenTiny

欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~

OpenTiny 官网opentiny.design
OpenTiny 代码仓库github.com/opentiny
TinyVue 源码github.com/opentiny/ti...
TinyEngine 源码: github.com/opentiny/ti...

欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI、TinyEditor~ 如果你也想要共建,可以进入代码仓库,找到 good first issue 标签,一起参与开源贡献~

相关推荐
Silver〄line2 分钟前
以鼠标位置为中心进行滚动缩放
前端
LaiYoung_3 分钟前
深入解析 single-spa 微前端框架核心原理
前端·javascript·面试
Danny_FD1 小时前
Vue2 + Node.js 快速实现带心跳检测与自动重连的 WebSocket 案例
前端
uhakadotcom1 小时前
将next.js的分享到twitter.com之中时,如何更新分享卡片上的图片?
前端·javascript·面试
韦小勇1 小时前
el-table 父子数据层级嵌套表格
前端
奔赴_向往1 小时前
为什么 PWA 至今没能「掘进」主流?
前端
小小愿望1 小时前
微信小程序开发实战:图片转 Base64 全解析
前端·微信小程序
掘金安东尼1 小时前
2分钟创建一个“不依赖任何外部库”的粒子动画背景
前端·面试·canvas
电商API大数据接口开发Cris1 小时前
基于 Flink 的淘宝实时数据管道设计:商品详情流式处理与异构存储
前端·数据挖掘·api
小小愿望1 小时前
解锁前端新技能:让JavaScript与CSS变量共舞
前端·javascript·css