作者:彭莉,火山引擎 APM 研发工程师。2020年加入字节,负责前端监控 SDK 的开发维护、平台数据消费的探索和落地。
这篇文章就是以此为背景,介绍字节内部是如何衡量站点性能的,如何依靠性能监控定位线上站点性能问题的。
站点性能一般可以分为两类,一类是首屏性能,另一类是运行时性能。前者衡量的是页面从加载开始到可以稳定交互的性能情况,后者衡量的是页面稳定后到页面关闭的性能情况。
那么针对用户感知到的这四个阶段,有没有可用于衡量的指标呢?
这两个指标都来源于 Paint Timing[2] 标准, 这个标准主要是记录在页面加载期间的一些关键时间点。通过这两个指标,就可以衡量页面何时开始渲染内容了。
有了这三个指标,就可以衡量页面何时渲染出主要内容了。不过业界有测试得出, LCP 非常近似于 FMP 的时间点,同时 FMP 性能消耗较大,且会因为一些细小的变化导致数值巨大波动,所以推荐使用 LCP。而 SI 因为计算复杂,指标难以解释,所以一般只在实验室环境下使用。
TTI 虽然可以衡量页面可以交互的时间点,但是却无法感知这个期间浏览器的繁忙状态。而结合 TBT ,就能帮助理解在加载期间,页面无法响应用户输入的时间有多久。
至此,通过上面每个阶段的指标,基本可以实现全面衡量首屏性能。那么运行时的性能又可以怎样衡量呢?
运行时性能一般可以通过Long tasks 和 Input Delay来感知。Long tasks主要是衡量主线程的繁忙情况,而 Input Delay 主要是衡量用户交互的延迟情况。
如果一个任务在主线程上运行超过 50 毫秒,那么它就是 Long task。如果可以收集到运行时的所有Long tasks,就能知道运行时的性能情况。在具体实践中,可以关注耗时较长的Long task,将它和用户行为关联在一起,可以有效帮助定位线上卡顿的原因。
它源于 Event Timing[3] 标准,这个标准主要是帮助深入了解由用户交互触发的某些事件的延迟,通过计算用户输入和处理输入后的页面绘制时间的差值来感知延迟时间。这些延迟通常是由于开发人员代码编写不当,引起 JS 执行时间过长而产生的。
页面性能相关的指标都有了,那么如何采集这些数据呢?
页面加载过程中的时间点主要依赖 Navigation Timing[4] 标准,这个标准后来升级到了2.0版本, 对应 Navigation Timing 2[5] 标准,两者虽然不尽相同,但是可计算出的指标基本一致。在浏览器中可以通过下面的方式获取:
// navigation timing
const timing = window.performance.timing
// navigation timing 2
performance.getEntriesByType('navigation')
基于这些数据,不仅可以计算出 DNS / TCP / Request 等耗时,还可以计算出 DOMReady / DOMParse / Load 等耗时。
FP 和 FCP 可以通过浏览器提供的 API 直接获取,所以采集原理并不复杂。如果页面已经完成了首次绘制和首次内容绘制,可以使用下面的方式直接获取。
window.performance.getEntriesByType('paint')
// or
window.performance.getEntriesByName('first-paint')
window.performance.getEntriesByName('first-contentful-paint')
但是如果页面还没有开始首次绘制,就需要通过监听获取。
const observer = new PerformanceObserver(function(list) {
const perfEntries = list.getEntries();
for (const perfEntry of perfEntries) {
// Process entries
// report back for analytics and monitoring
// ...
}
});
// register observer for paint timing notifications
observer.observe({entryTypes: ["paint"]});
LCP 主要依赖 PerformanceObserver,具体监听方式如下:
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('LCP candidate:', entry.startTime, entry);
}
}).observe({type: 'largest-contentful-paint', buffered: true});
浏览器会多次报告 LCP ,而一般真正的 LCP 是用户交互前最近一次报告的 LCP ,因为交互往往会改变用户可见的内容,所以用户交互后新报告的 LCP 不再符合 LCP 的指标定义。
窗口前的最后一个长任务的结束时间就是 TTI 。
阻塞时间是 Long task 中超过 50ms 后的任务耗时。
FID 同样依赖 PerformanceObserver,具体监听方式如下:
new PerformanceObserver(function(list, obs) {
const firstInput = list.getEntries()[0];
// Measure the delay to begin processing the first input event.
const firstInputDelay = firstInput.processingStart - firstInput.startTime;
// Measure the duration of processing the first input event.
// Only use when the important event handling work is done synchronously in the handlers.
const firstInputDuration = firstInput.duration;
// Obtain some information about the target of this event, such as the id.
const targetId = firstInput.target ? firstInput.target.id : 'unknown-target';
// Process the first input delay and perhaps its duration...
// Disconnect this observer since callback is only triggered once.
obs.disconnect();
}).observe({type: 'first-input', buffered: true});
MPFID 是 FCP 之后最长的长任务耗时,可以通过监听 FCP 之后的 Long tasks,对比拿到最长的长任务就是 MPFID 。
虽然浏览器提供了足够的 API 来帮助采集各个性能指标,但是在 JS 中计算具体指标要更为复杂。原因有两点:一是 API 报告的内容和指标本身的定义有部分差异,所以计算时要处理这些差异;二是 部分场景下浏览器不会报告对应内容,这些场景下需要模拟测量。
虽然有众多性能指标,但是每个性能指标评估的都是单一方面,如何整体来看站点的性能是好是坏呢?对于每个单一指标,是否有标准去定义指标的值在具体哪个范围内能算优秀?线上的站点性能应该重点考量哪些性能指标?各个性能指标的权重占多少合适呢?
下方是目前字节内部使用的部分性能指标基准线,基本对齐 Google 建议的基准线,通过这些数据可以分析站点的性能达标率情况。
站点满意度的衡量除了要考虑常规的性能指标外,还要考虑体验类的指标,比如专门衡量视觉稳定性的指标 CLS。
线上的站点满意度衡量,一般会在参考lighthouse的满意度计算规则的基础上,去除一些推荐在实验室环境测量的指标的权重。
下方是目前字节使用的线上站点性能满意度的权重计算公式,去除了SI 和 TBT这两个不推荐在线上环境测量的指标。
那么有了一个站点满意度以后,我们终于能知道一个站点的性能好坏了。那么假设性能不好,我们应该怎样优化?
扫描下方二维码,立即申请免费使用⬇️
阅读原文了解【应用性能监控全链路版】,关注公众号【火山引擎开发者服务】回复【交流群】可加入应用性能监控交流群
[1] Web性能工作组:https://www.w3.org/webperf/
[2] Paint Timing:https://w3c.github.io/paint-timing/
[3] Event Timing: https://w3c.github.io/event-timing/
[4] Navigation Timing: https://www.w3.org/TR/navigation-timing/
[5] Navigation Timing 2: https://www.w3.org/TR/navigation-timing-2/