写在最前:Performance API是一个通过JS来测量和评估性能指标的标准集合。想象它是一个囊括了所有统计Web核心指标的相似功能,和通过很多性能测试工具生成的性能数据的盒子。本文会示范如何通过Performance API在DOM层面生成性能指标。
最近几周性能社区因为一件事变得很热闹——Interaction to Next Paint(INP)正式成为Core Web Vitals(CWV)的官方指标。解释一下,INP正在取代First Input Delay(FID)这个指标,更多细节你可以看Smashing Magazine的介绍。
但是,指标不是我最关心的,我最关心的还是性能,我决定重新前往MDN重新审视Performance API,我们可以用它来通报页面中元素的加载时间,甚至可以实时通报CWV的指标,接下来我们看几个API的使用案例。
浏览器支持说明:Performance API因其囊括了大量不同的接口、属性和方法,大部分的属性被主流的浏览器支持,基于Chromium的浏览器支持全部的CWV的属性,Firefox支持First Contentful Paint(FCP)和Largest Contentful Paint(LCP)属性。
首先,访问数据
有两种方式可以让我们检索到我们关注的性能指标:
- 使用
performance.getEntries()
方法 - 使用
PerformanceObserver
实例
而PerformanceOberserver
有一些重要的优势:
PerformanceObserver
可以实时观察性能指标和发布指标,而performance.getEnties()
总是返回一个实体列表,从开始记录性能指标开始的性能实体列表。PerformanceObserver
是异步发布性能,这意味着它不会阻塞浏览器的行为performance.getEntries()
方法在Element
的性能指标方面不工作
上述都在阐述一个事实,创建一个PerformanceObserver
吧:
const lcpObserver = new PerformanceObserver(list => {})
我们先传递一个空的callback给PerformanceObserver构造函数。之后我们会换成我们需要处理的性能指标对应的方法。现在,我们开始观察:
lcpObserver.observe({type: "largest-contentful-paint", buffered: true})
这个代码片段中最重要的是buffered: true
这个属性,将其设置为true
不仅仅我们想要获取开始观察后得到的性能指标,而且我们也想得到开始观察前在浏览器缓存的性能指标的列表。
其次是我们在使用largest-contentful-paint
属性,这就是性能 API 的酷炫之处:它可用于衡量非常具体的事物,但也支持直接映射到 CWV 指标的属性。我们先看看这个LCP属性。
上报LCP(largest-contentful-paint)
largest-contentful-paint
属性观测页面中的一切,识别初始视图中最大的内容区并且计算它的加载时间。换言之,我们在观测整个页面加载并获取页面中最大部分的渲染数据。
我们已经有了性能观测对象和callback:
const lcpObserver = new PerformanceObserver(list => {});
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
接下来,我们填充这个callback,一旦性能开始检测,它会返回一个实体列表:
// The Performance Observer
const lcpObserver = new PerformanceObserver(list => {
// Returns the entire list of entries
const entries = list.getEntries();
});
// Call the Observer
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
此时,我们需要知道哪个元素才是LCP。值得注意的是,在这个实体列表中的最后一个元素就是代表着LCP元素。因此,我们可以直接返回列表中的最后一个实体:
// The Performance Observer
const lcpObserver = new PerformanceObserver(list => {
// Returns the entire list of entries
const entries = list.getEntries();
// The element representing the LCP
const el = entries[entries.length - 1];
});
// Call the Observer
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
最后就是展示结果,我们可以创建一些dashboard界面来消费这些数据并通过一个美观地方式进行展现,这里我们就简单地通过Log打印了哈:
// The Performance Observer
const lcpObserver = new PerformanceObserver(list => {
// Returns the entire list of entries
const entries = list.getEntries();
// The element representing the LCP
const el = entries[entries.length - 1];
// Log the results in the console
console.log(el.element);
});
// Call the Observer
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
来看看实际效果:
我们可以很直观地看到最大的元素是哪个,但是我还想看到更多信息,比如这个最大元素的渲染时间是多少呢:
// The Performance Observer
const lcpObserver = new PerformanceObserver(list => {
const entries = list.getEntries();
const lcp = entries[entries.length - 1];
entries.forEach(entry => {
// Log the results in the console
console.log(
`The LCP is:`,
lcp.element,
`The time to render was ${entry.startTime} milliseconds.`,
);
});
});
// Call the Observer
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
// The LCP is:
// <h2 class="author-post__title mt-5 text-5xl">…</h2>
// The time to render was 832.6999999880791 milliseconds.
上报LCP(First Contentful Paint)
这是关于一个DOM元素在屏幕上渲染的时间,当然是越快越好,在LightHouse的标准里面,这个值的及格线在0到1.8s之间。
就像上一个部分中我们通过设置type
属性为largest-contentful-paint
来获取性能数据,这次我们将会设置一个不同的值:paint
。
当我们讨论paint
,从上述的PerformancePaintTiming
界面中可以得出其内容为:first paint和first contentful paint的值。
// The Performance Observer
const paintObserver = new PerformanceObserver(list => {
const entries = list.getEntries();
entries.forEach(entry => {
// Log the results in the console.
console.log(
`The time to ${entry.name} took ${entry.startTime} milliseconds.`,
);
});
});
// Call the Observer.
paintObserver.observe({ type: "paint", buffered: true });
// The time to first-paint took 509.29999999981374 milliseconds.
// The time to first-contentful-paint took 509.29999999981374 milliseconds.
上述结果中吐出了两个结果:一个是first-paint
,另一个是first-contentful-paint
。我知道这个时间大部分发生在用户进入到这个页面到页面执行渲染之间,但是我并不清楚这两个指标之间的不同。
那么我们看看官方的解释:
二者的核心区别是:First Paint标志着浏览器初次渲染文档提供的任何内容,相反地,First Contentful Paint则是标志着浏览器初次从DOM中渲染图片或者文本的第一个字节
从结果来看,我在上面这个例子中这俩指标我得到了一个相等的结果。因为first paint可以是任何内容,例如:一个背景色。我认为,相同的结果意味着,无论什么内容第一次被绘制到屏幕上,恰好也是第一次有内容的绘制。
但实际上,二者还是有很多细微的差别的。就比如Chrome中不同版本在FCP上的值是不同。谷歌官方有记录这些内容变化,所以当你评估这些值的时候记得这个变化,尤其当你发现你测试到的值跟团队里其他人不同的时候。
累计视图偏移(Cumulative Layout Shift)
当元素渲染到页面的时候,页面发生了多少偏移呢?我们可以通过Performance API得到答案,这次我们设置type为layout-shift
。
这个指标相较于其他指标更加冒险,LayoutShift
仍然在一个实验性的阶段,且Chrome是唯一值的支持该指标的。
就目前情况来看,LayoutShift
提供了一些信息:value
代表着偏移量,包括了哪些资源导致了这个值。不仅于此,我们可以识别是否是用户的交互影响了这个CLS值,例如缩放,调整浏览器的尺寸,或者回车键、选中键等。lastInputTime
属性有一个对等的属性hasRecentInput
——这个属性为boolean类型,当lastInputTime
的值小于500ms的时候,hasRecentInput
的值即为true
。
明白了吗?这个属性不仅可以帮助我们了解页面加载中偏移量的值,也可以找出罪魁祸首,同时排除用户交互导致的任何偏移。
const observer = new PerformanceObserver((list) => {
let cumulativeLayoutShift = 0;
list.getEntries().forEach((entry) => {
// Don't count if the layout shift is a result of user interaction.
if (!entry.hadRecentInput) {
cumulativeLayoutShift += entry.value;
}
console.log({ entry, cumulativeLayoutShift });
});
});
// Call the Observer.
observer.observe({ type: "layout-shift", buffered: true });
考虑到这是一个实验特质的指标,下面是请求以后得到的entry
对象:
很简单清晰对吧,我们不仅可以看到偏移量的多少(0.128
),可以看到哪个元素引发的偏移(article.a.main
),而且还可以看到元素准确的坐标位置,从哪里开始到哪里结束。
用户交互时间(Interaction To Next Paint)
这个新指标让我在一开始对性能API产生了疑惑,INP在一段时间的考察后,让其取代FID(First Input Delay),并在2024年3月变成CWV指标成为可能。关于INP的定义:在用户与页面交互到页面响应这个交互的时间。
针对这个指标我们需要深入到PerformanceEventTiming
类。在用户交互层面有很多内容可以挖掘,什么类型的时间触发了(entryType
和name
),什么时候触发的(startTime
),什么元素触发了这个交互(interactionId
,实验性的),交互的过程的开始(processingStart
)和结束(processingEnd
)分别是什么时候。用户还可以将某个交互取消掉(cancelable
)。
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
// Alias for the total duration.
const duration = entry.duration;
// Calculate the time before processing starts.
const delay = entry.processingStart - entry.startTime;
// Calculate the time to process the interaction.
const lag = entry.processingStart - entry.startTime;
// Don't count interactions that the user can cancel.
if (!entry.cancelable) {
console.log(`INP Duration: ${duration}`);
console.log(`INP Delay: ${delay}`);
console.log(`Event handler duration: ${lag}`);
}
});
});
// Call the Observer.
observer.observe({ type: "event", buffered: true });
长动画帧(Long Animation Frames)
最后一个指标,我们现在可以在网页中跟踪INP的值,并且可以将其定位到特定的组件中。但是因为哪个代码的执行导致了这些延迟呢?
Long Animation Frames API就是用来解答这个问题的。在2024年3月中旬的版本里这个功能还不是稳定版本,不过你可以在灰度版本里面使用了。
long-animation-frame
入口在每次浏览器因其他进程阻塞导致无法直接渲染页面的时候进行上报。我们不仅可以得到全部的长帧的持续时间,还能得到在当前进程中引入的不同scripts
的时间。
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.duration > 50) {
// Log the overall duration of the long frame.
console.log(`Frame took ${entry.duration} ms`)
console.log(`Contributing scripts:`)
// Log information on each script in a table.
entry.scripts.forEach(script => {
console.table({
// URL of the script where the processing starts
sourceURL: script.sourceURL,
// Total time spent on this sub-task
duration: script.duration,
// Name of the handler function
functionName: script.sourceFunctionName,
// Why was the handler function called? For example,
// a user interaction or a fetch response arriving.
invoker: script.invoker
})
})
}
});
});
// Call the Observer.
observer.observe({ type: "long-animation-frame", buffered: true });
当页面中的INP交互发生的时候,我们可以找到最接近的长动画帧,并研究是什么处理延迟了页面响应。
介绍一个包
性能API是如此庞大和强大。我们可以很容易地花整个训练营的时间来学习所有的接口以及它们提供的内容。除了我们所看到的核心Web Vitals之外,还有网络计时、导航计时、资源计时和大量自定义报告功能。
如果CWV是您真正想要的,那么您可能会考虑查看web-vitals库来包装浏览器性能API。
现在,你只需要一个方法:
webVitals.getINP(function(info) {
console.log(info)
}, { reportAllChanges: true });
reportAllChanges
属性是什么作用?这个属性的意思是:每次指标的值发生变化就进行上报,而不是指标得到最终值的时候才上报。例如,一旦页面打开,总会有一次机会用户会遇到一个比当前INP交互更慢的交互。所以,如果没有reportAllChanges
,只有在页面关闭了(或者当用户切换浏览器的tab进行页面隐藏)我们才会看到INP的上报。
我们也可以仅上报初次结果和由此产生变化之间的差异。例如:
function logDelta({ name, id, delta }) {
console.log(`${name} matching ID ${id} changed by ${delta}`);
}
onCLS(logDelta);
onINP(logDelta);
onLCP(logDelta);