本文作者:牟牟,泽白,菉竹
背景
性能优化是每一个工程师都必不可少的能力,只有把更多的细节优化放在日常开发与设计中,才可能尽量延缓一个持续迭代的系统成为巨石应用,而导致后续优化无从下手。本文将介绍如何通过现有的工具分析并量化一个前端页面的性能情况,希望对大家进行针对性的页面优化时能提供一些帮助。
性能指标
衡量一个 Web 页面的体验和质量有很多指标,我们先来了解一下社区和集团内分别有哪些常用的指标。
社区指标
经过多年的发展,Google 提供了很多工具来衡量和报告 Web 的性能,目前的核心指标即是 Core Web Vital,这些指标包括加载体验(LCP)、交互性(FID)和页面内容的视觉稳定性(CLS),每个指标都是可衡量并且设定了一个良好的测量阈值,这些指标同样可以通过 Chrome DevTools 中的 LightHouse 面板进行测量。
LCP
Largest Contentful Paint (LCP) :该指标会根据页面 首次开始加载的时间点 来报告可视区域内可见的最大 图像或文本块 完成渲染的相对时间。即浏览器可视区域中最大的内容块渲染出来的时间,用以测量加载性能。
首次开始加载的时间点 是指 PerformanceNavigationTiming.startTime,图像或文本块 考量的元素包含 <img>
、<svg>
元素内的 <image>
元素、<video>
元素(使用封面图像)、文本节点等,示例如下
从上图可以看出最大内容元素随着页面渲染是在不断变化的,在页面的渲染过程中,完成渲染(图片元素需要加载完成)的最大可视元素会替换掉旧的最大内容元素,则最新的最大可视元素时间点即为 LCP 的时间点。
FID
First Input Delay (FID) :考察从用户首次与页面进行交互(即当他们单击链接,点击按钮或使用自定的 JavaScript 驱动的控件)到浏览器实际上能够响应该交互之间的时间,测量交互性。
通过下图可以看出 FID 是在 TTI 的时间内第一个互动事件的开始时间与浏览器回应事件的时间差。
在上方的示例中,用户恰好在主线程刚进入最繁忙的时段时与页面进行了交互。如果用户稍微提早一点(在空闲期间)与页面进行交互,那么浏览器就会立即响应。输入延迟上的这种差异强调了在分析该指标时,查看指标分布情况的重要性。
CLS
Cumulative Layout Shift (CLS) :累积布局偏移,测量视觉稳定性,指网页加载时元素上下跳动情况的衡量指标。
通过下图可以看出当用户点击时,元素位置发生变化导致点击错误,是很差的用户体验。这类出现元素跳动的场景,都会影响 CLS 分数。
CLS 的计算公式是:
布局偏移分数 = 影响分数 * 距离分数
,让我们通过实际场景了解一下是如何计算的:
在上图中,有一个元素在一帧中占据了一半的可视区域。接着,在下一帧中,元素下移了可视区域高度的 25%。红色虚线矩形框表示两帧中元素的可见区域集合,在本示例中,该集合占总可视区域的 75%,因此其影响分数为0.75 。最大的可视区域尺寸维度是高度,不稳定元素的位移距离为可视区域高度的 25%,因此距离分数为 0.25。所以布局偏移分数是
以上三个核心指标同样可以在页面的运行时进行获取,只需在页面中引入 web-vitals 库即可查看页面核心指标情况。
分析工具
Performance Tool
Chrome Devtools Performance Tool 是一款谷歌浏览器自带的性能分析工具,功能十分强大,通常被用来定位和排查 web 页面的性能瓶颈,因此对它的使用也是我们需要掌握的性能分析技能之一。
接下来,本文将通过分析今日爆款页面首屏加载的过程,介绍如何使用 Performance 工具来排查该页面可能存在的性能问题。
数据采集
在分析页面的性能之前,我们需要先用 Performance 工具的录制功能来记录页面所发生的行为。
为了保证一个相对干净的页面执行环境(排除 浏览器插件、浏览器缓存 等影响),请务必在隐私浏览模式下打开待分析性能的 web 页面。接着,请打开页面控制台并切到 Performance 面板。 注:我们需要将 CPU 选项调至 6x slowdown 来模拟更低端的机型,这样能够更容易暴露出性能问题。
注意面板左上角的三个按钮:
- 按钮 1:开始录制
- 按钮 2:刷新页面并录制
- 按钮 3:清除之前已录制的内容
基于此,Performance 工具提供了两种录制方式:
- 点击 按钮 1,开始页面的交互操作,交互结束后点击 stop 按钮,录制结束。
2. 点击 按钮 2,等候片刻(期间不要进行任何操作,浏览器会自动刷新页面),等页面加载完毕后会自动结束录制。(注:相较于前一种录制方式,该方式仅适用于录制页面的首屏加载)
由于本文以今日爆款页面首屏加载性能分析为例,故采用第二种方式进行录制。下图即为使用第二种方式录制的 profile 展示:
数据分析
接下来,本文将继续介绍如何查看 Performance 面板录制的内容。
还是以录制好的今日爆款页面首屏加载过程为例,Performance 面板的 network 区域会按时间线的先后顺序向我们展示浏览器加载的所有资源。基于此,我们就能以时间线展开的方式,了解页面从加载到渲染所经历的各个阶段以及各自的开销耗时,从而发现性能的瓶颈所在。
一,文档请求
如上所示,浏览器在最一开始发起了 html 文档请求,该过程一共花费了 263ms。
分析: 影响该阶段时间的主要因素是 html 文档大小 以及 用户当时所处的网络环境。由于我们无法改变用户所处的网络环境,所以可以做得就是尽量精简 html 文档的大小。
二,首屏数据请求
如上所示,浏览器在加载完 html 文档后过了 250ms 才发起首屏数据请求。
分析: 在上述这段请求等待时间内,浏览器解析到 html 文档中的 link 和 script 标签时会发起对应的 css 和 js 资源请求,等资源加载完成之后再解析并执行它们。因此,影响该阶段时间的主要因素有 2 点:
- css/js 等静态资源的加载快慢:针对这个问题,我们可以使用本地缓存(比如 zcache)的方式来减小这部分的时间开销。
- 发起数据请求代码执行的时机:根据上图所示,数据请求是在执行第二个脚本(业务主 bundle 代码)时才发起的。而此时已经来到了 634ms,距离文档加载完成已经过去了 371ms。因此,我们应该尽可能将数据请求前置,来减少请求等待所花费的时间。
如上所示,从 浏览器发起首屏请求 到 完全接收请求响应 一共花费了 360ms。
分析: 影响该阶段时间的主要因素有 2 点:
- 服务端 RT:需要服务端接口优化其处理时间。
- 报文大小:报文越大,浏览器接收报文的时间就越长,因此精简报文大小也能减小该阶段的时间开销。
三,首屏文档渲染
如上所示,浏览器首屏数据加载完成后经过 1.5s 左右才完成首屏关键路径的渲染(不包含图片)。
分析:
根据图中 Main 区域可知,请求完成 到 渲染首屏 中间有一段非常耗时的 js 脚本执行。这是因为该页面的内容渲染依赖 rax,而 rax 需要先生成虚拟 dom,再将虚拟 dom 映射到真实 dom,这时浏览器才开始渲染绘制页面首屏的内容。可以看到仅脚本执行就花费了 1.2s 左右,说明这段 js 脚本执行有很大的优化空间。
四,首屏图片上屏
如上所示,浏览器在解析首屏内容时遇到图片元素就会发起相应的图片请求,而从发起图片请求到图片完全上屏的开销在 1s 以上。
分析: 开始加载图片 与 关键路径渲染完成 的时间点并不完全重合,但是却与前一个 js 脚本执行完成的时间点吻合。这是因为前一个 js 脚本为 rax 将真实 dom 插入至 id 为 root 的 div 操作,此时浏览器会 parseHTML,发现有图片元素就会发起相应的图片请求,而不是等到首屏帧绘制完成才发起请求。
明白该问题之后,我们再来看加载图片资源各个阶段分别是什么含义。根据 Google 官方文档 介绍:
- The left line is everything up to the Connection Start group of events, inclusive. In other words, it's everything before Request Sent, exclusive.
- The light portion of the bar is Request Sent and Waiting (TTFB).
- The dark portion of the bar is Content Download.
- The right line is essentially time spent waiting for the main thread. This is not represented in the Timing tab.
根据文档描述,上图中加载图片资源的网络传输时间(即深色绿色块)实际占比很少,绝大部分时间都在最后一个阶段(spent waiting for the main thread,被主线程阻塞)。这是因为这期间虽然图片内容早已下载完成,但仍有部分 decode 操作由于主线程被其他 js 脚本占用着而迟迟无法执行,导致图片一直无法上屏。
由此我们可以得知,影响图片上屏的主要因素有 2 个:
- 图片大小:需要开启图片大小优化,减少图片的网络传输时间。
- js 执行阻塞:应该尽量将一些 非关键 js 操作延后 或 同步操作异步化,尽量给图片上屏留出空隙时间。
至此,如何使用 Performance 工具来分析 今日爆款页面首屏加载 性能的例子就介绍完了:本文按时间线先后顺序展开的方式分析了今日爆款页面首屏加载各个阶段所存在的问题,并给出了相应的优化措施和建议。当然了,本文所介绍的 Performance 工具使用仅仅只是冰山一角,更多了解可以翻阅 Goolgle 官方文档 查看。
JavaScript Profiler
通过 Performance Tool 的火焰图,我们已经能够发现页面性能的问题点,不过火焰图是性能数据的直观展示,当我们想了解更精确的性能数据,或者我们着手去优化代码,需要精确评估优化效果时,只使用 Performance Tool 的火焰图就不够了。其实 Chrome 的 debug 工具中就有另一个非常好用的 js 脚本性能分析工具:JavaScript Profiler,就能帮我们解决这个问题。它的入口在这里:
打开之后是这样的:
点击 Start 按钮或者控制台左上角的 ○ 按钮进行性能采样。操作页面或者等待一段时间后,点击 Stop 或者再次点击 ○ 结束采样。
Self Time:表示函数自身执行的时间,不包括在函数内调用的其他函数耗时。
Total Time: 表示函数整体执行的时间,包括函数内调用的其他函数耗时。
Function:函数名以及对应的代码位置,(idle) 和 (program) 分别表示空闲时间和浏览器自身耗时。
分析流程
分析 js 脚本性能的一般工作流是这样的:
- 点击
Start
按钮开始性能采样 - 操作需要分析的功能点
- 点击
Stop
停止采样 - 分析采样数据
如果想分析某段具体代码的性能,可以通过 console.profile
自动采样:
console.profile('profile1');
doSomething();
console.profileEnd('profile1');
这样脚本运行到代码处,就会自动进行采样,产生一个名为 profile1
的采样记录,我们就可以直接开始性能分析了。
实操案例
我们以一个实际的场景为例来说明如何使用 CPU Profiler 实现性能优化:我们实现了一个页面帧率采样的 npm 包,需要做性能分析和优化。
在这个场景下,我们有一个 ScrollEventMonitor
类,它有一个成员方法 scrollHandle
用于处理 scroll 事件,判断滑动是否结束,代码如下:
class ScrollEventMonitor {
// 处理 scroll 事件的方法
scrollHandle(event) {
this.scrollTimeStamp = performance.now();
if (process.env.NODE_ENV !== 'production') {
debug.log('scroll');
}
if (!this.isScrolling) {
this.fpsCollector.observe();
this.isScrolling = true;
}
clearTimeout(this.scrollTimer);
this.scrollTimer = setTimeout(() => {
if (performance.now() - this.scrollTimeStamp > this.scrollTimeOut) {
// 停止滚动情况
// clearInterval(this.scrollTimer);
this.fpsCollector.cancel();
this.isScrolling = false;
if (process.env.NODE_ENV !== 'production') {
debug.log('scroll end');
}
this.isScrolling = false;
}
}, this.scrollTimeOut);
}
}
要评估这段代码的性能,我们需要通过 CPU Profiler 进行定量分析。 第一步:采样,打开 CPU Profiler,点击 Start,开启性能采样。
第二步,由于我们的函数调用是通过屏幕滚动触发的,因此我们需要滑动页面,触发 js 代码运行。
第三步,滚动一段时间后,停止采样。
第四步,在右侧树中找到待分析的函数,如果内容比较多,可以通过 Command + F 查找,找到函数后选中它,然后点击顶部的 Focus 按钮:
此时,选中的节点会变成整棵树的根节点,所有时间百分比统计会按照根节点为 100% 重新计算。
这样我们就很容易发现其中的性能热点了:
scrollHandle
函数自身占用 CPU 时间为 46.15%,而 setTimeout 和 clearTimeout 操作占整个函数耗时的 36%。
每次 scroll 事件都进行 timeout 的 clear 和 set 操作,在我们这个场景下是不太合理的,可以通过改为 setInterval 减少任务队列操作,虽然这会带来超时窗口不准确的副作用,但这在当前场景下是可接受的,因此我们把代码改为:
class ScrollEventMonitor {
// 处理 scroll 事件的方法
scrollHandle(event) {
this.scrollTimeStamp = performance.now();
if (process.env.NODE_ENV !== 'production') {
debug.log('scroll');
}
// 首次执行
if (!this.isScrolling) {
this.fpsCollector.observe();
// 注册轮询事件检测是否停止滚动
this.scrollTimer = setInterval(() => {
if (performance.now() - this.scrollTimeStamp > this.scrollTimeOut) {
// 已停止滚动
clearInterval(this.scrollTimer);
this.fpsCollector.cancel();
this.isScrolling = false;
if (process.env.NODE_ENV !== 'production') {
debug.log('scroll end');
}
}
}, this.scrollTimeOut);
this.isScrolling = true;
}
}
}
修改后重新进行性能采样,结果如下:
scrollHandle
函数自身占用的 CPU 时间从 46.15% 上升到 86.36%,而两个版本的 scrollHandle
代码流程是基本一致的,setTimeout、clearTimeout、setInterval 函数执行时间不会统计到 self time 中,那么我们可以得到两个等式:
其中 m1 为优化前总耗时,m2 为优化后总耗时。然后就能推出:
意味着相比优化前的版本,
scrollHandle
函数总耗时降低了 46.6%。
网络库耗时分析工具
在前文中提到的数据请求阶段,作为前端开发者,我们其实并不了解整个过程中各个环节的耗时,因此也无法精准得判断出究竟是 网络问题、服务端 RT、报文大小 还是网络库本身 导致数据请求慢。因此,我们需要一个网络库耗时分析工具来得知每个环节的耗时。 需要注意的是,我们的 H5 页面在不同的浏览器环境内,mtop 数据请求的方式也是不一样的:
- 端内(手淘等客户端):数据请求走 WindVane 的网络库
- 端外(其他浏览器):数据请求走的是 jsonp 协议
针对端内的数据请求场景,客户端是有打全链路日志的。因此我们也可以根据这些数据来分析数据请求阶段中各个环节的耗时。接下来,本文就将介绍如何分析端内 mtop 请求的网络库耗时。
数据采集
- Android 机数据线连接电脑,adb 连接成功
- 执行以下命令,打开分析开关
$ adb shell touch /data/local/tmp/.com_taobao_taobao_fulltrace_switcher
- 执行以下命令,重定向手机日志至电脑本地 data.txt
$ adb logcat | grep analysis.FullTraceAnalysis > data.txt
- 手机打开 H5 页面,正常浏览访问
至此,日志就采集完毕了。
数据分析
- 下载 netflow 工具
$ git clone git@gitlab.alibaba-inc.com:network-tool/netflow.git
$ cd netflow
$ tnpm i
- 拷贝日志文件 data.txt 覆盖项目根目录自带的 data.txt 文件
- 执行以下命令,打开可视化工具
$ npm run dev
- 浏览器访问访问 http://localhost:8090
根据图表所示,我们就能看到数据请求过程中每个环节的耗时,从而更准确地知道究竟是哪个环节导致数据请求慢了。
总结
文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞分享。