RAIL性能评估模型从以下四个方面提出了要求:
- 立即响应用户;在 100 毫秒以内确认用户输入。
- 设置动画或滚动时,在 10 毫秒以内生成帧。
- 最大程度增加主线程的空闲时间。
- 持续吸引用户;在 1000 毫秒以内呈现交互内容。
针对该评估模型,Google 提出了以用户为中心的四个衡量指标:
- Is it happening? First Paint (FP) / First Contentful Paint (FCP)
- Is it useful? First Meaningful Paint (FMP) / Hero Element Timing
- Is it usable? Time to Interactive (TTI)
- Is it delightful? Long Tasks
分别对应渲染过程中的若干阶段,截图如下:
那么如何具体统计这些指标呢?
旧方法的问题
过去的某些统计方法是会损耗性能的,例如使用 rAF 检测过长的帧。但是缺点很明显,轮询会影响性能。
(function detectLongFrame() {
var lastFrameTime = Date.now();
requestAnimationFrame(function() {
var currentFrameTime = Date.now();
if (currentFrameTime - lastFrameTime > 50) {
// Report long frame here...
}
detectLongFrame(currentFrameTime);
});
}());
下面来看看具体针对这四个指标的检测方法以及优化方式。
FP/FCP
跟踪 FP/FCP,监听 paint 事件,不得不说这样的确太方便了。
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// `name` will be either 'first-paint' or 'first-contentful-paint'.
const metricName = entry.name;
const time = Math.round(entry.startTime + entry.duration);
ga('send', 'event', {
eventCategory: 'Performance Metrics',
eventAction: metricName,
eventValue: time,
nonInteraction: true,
});
}
});
observer.observe({entryTypes: ['paint']});
针对 FP,也就是优化首屏方案大致包括以下几种,当然实现难度各异:
- 减少 head 中阻塞的 JS/CSS,这一点已经普遍应用,包括关键路径资源等
- HTTP/2 push,这就对服务端提出很高要求了,我发现目前 Ele.me 已经应用
- app shell,整个应用的壳,抽离出来利于离线缓存,Lavas 中已经应用
FMP
关于页面有效内容,或者“Hero element”,由于依赖具体实现,并没有给出通用方法。 具体可以使用performance api度量指标。
TTI
这个指标我第一次听说,首次可交互时间。不过其实在前端渲染完成之前,例如展示 skeleton 页面骨架时,对于用户而言就是无法交互的状态,只能看不能点。
文章中指出在添加到 PerformanceObserver 之前,可以使用polyfill完成:
import ttiPolyfill from './path/to/tti-polyfill.js';
ttiPolyfill.getFirstConsistentlyInteractive().then((tti) => {
ga('send', 'event', {
eventCategory: 'Performance Metrics',
eventAction: 'TTI',
eventValue: tti,
nonInteraction: true,
});
});
long task
浏览器在响应用户交互事件时,向队列中添加任务,等待主线程依次执行。 由于主线程还要负责执行 JS,当处理时间过长时,就会导致任务无法及时得到执行,给用户的感觉就是未响应。 通常定义超过 50ms 响应时间的任务就是 long task 了。
和 FP 一样,可以直接使用 PerformanceObserver:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
ga('send', 'event', {
eventCategory: 'Performance Metrics',
eventAction: 'longtask',
eventValue: Math.round(entry.startTime + entry.duration),
eventLabel: JSON.stringify(entry.attribution),
});
}
});
observer.observe({entryTypes: ['longtask']});
关于优化方式,可以使用requestIdleCallback,不重要的任务例如发送日志等操作可以放在里面执行。但是支持度不高。
input latency
滚动和动画的延迟是难以统计的,但是针对点击事件的响应延迟,可以采用如下方法统计:事件触发的时间到最终响应时的时间差就是延迟了,当超过 100 毫秒时进行记录:
const subscribeBtn = document.querySelector('#subscribe');
subscribeBtn.addEventListener('click', (event) => {
const lag = performance.now() - event.timeStamp;
if (lag > 100) {
ga('send', 'event', {
eventCategory: 'Performance Metric'
eventAction: 'input-latency',
eventLabel: '#subscribe:click',
eventValue: Math.round(lag),
nonInteraction: true,
});
}
});