了解如何在网站的现场数据中查找互动缓慢的问题,以便找出机会来提高 Interaction to Next Paint 得分。
现场数据可让您了解实际用户对您网站的体验。它可以找出您仅从实验数据中找不到的问题。在 Interaction to Next Paint (INP) 方面,现场数据对于识别互动缓慢至关重要,并可提供有价值的线索来帮助您解决问题。
在本指南中,您将了解如何使用 Chrome 用户体验报告 (CrUX) 中的实测数据快速评估您网站的 INP,以确定您的网站是否存在 INP 问题。接下来,您将学习如何使用 web-vitals JavaScript 库的归因 build(以及它通过 Long Animation Frames API (LoAF) 提供的新数据洞见)来收集和解读网站上互动缓慢的现场数据。
从 CrUX 开始评估您网站的 INP
如果您未从网站用户收集实测数据,CrUX 可能是不错的起点。CrUX 会从已选择发送遥测数据的真实 Chrome 用户那里收集实测数据。
CrUX 数据会显示在多个不同位置,具体取决于您要查找的信息范围。CrUX 可以针对以下各项提供 INP 和其他 Core Web Vitals 的数据:
- 使用 PageSpeed Insights 分析各个网页和整个来源。
- 网页类型。例如,许多电子商务网站都包含商品详情页和商品详情页面类型。您可以在 Search Console 中获取唯一网页类型的 CrUX 数据。
首先,您可以在 PageSpeed Insights 中输入您网站的网址。输入网址后,系统将针对多个指标(包括 INP)显示该网址的字段数据(如果有)。您还可以使用切换开关检查移动设备和桌面设备维度的 INP 值。
这些数据很有用,因为它们可以告诉您是否存在问题。不过,CrUX 无法告诉您导致问题的原因。有许多实时用户监控 (RUM) 解决方案可帮助您从网站用户那里收集自己的实测数据,以便回答此问题。其中一种方法是使用 web-vitals JavaScript 库自行收集实测数据。
使用 web-vitals
JavaScript 库收集字段数据
web-vitals
JavaScript 库是一种脚本,您可以将其加载到您的网站上,以便从网站用户那里收集现场数据。您可以使用它来记录多种指标,包括支持 INP 的浏览器中的 INP。
web-vitals 库的标准 build 可用于从现场用户获取基本 INP 数据:
import {onINP} from 'web-vitals';
onINP(({name, value, rating}) => {
console.log(name); // 'INP'
console.log(value); // 512
console.log(rating); // 'poor'
});
为了分析来自用户的现场数据,您需要将这些数据发送到某个位置:
import {onINP} from 'web-vitals';
onINP(({name, value, rating}) => {
// Prepare JSON to be sent for collection. Note that
// you can add anything else you'd want to collect here:
const body = JSON.stringify({name, value, rating});
// Use `sendBeacon` to send data to an analytics endpoint.
// For Google Analytics, see https://github.com/GoogleChrome/web-vitals#send-the-results-to-google-analytics.
navigator.sendBeacon('/analytics', body);
});
不过,这些数据本身提供的信息并不比 CrUX 提供的信息多。这正是 web-vitals 库的归因机制的用武之地。
进一步了解 web-vitals 库的归因 build
web-vitals 库的归因 build 会显示您可以从现场用户那里获取的额外数据,帮助您更好地排查影响网站 INP 的有问题的互动。您可以通过库的 onINP()
方法中显示的 attribution
对象访问此数据:
import {onINP} from 'web-vitals/attribution';
onINP(({name, value, rating, attribution}) => {
console.log(name); // 'INP'
console.log(value); // 56
console.log(rating); // 'good'
console.log(attribution); // Attribution data object
});
除了网页的 INP 本身之外,归因 build 还提供了大量数据,可帮助您了解互动缓慢的原因,包括您应重点关注互动过程的哪个部分。它可以帮助您解答重要问题,例如:
- “用户在网页加载期间是否与网页互动?”
- “互动事件处理脚本是否运行了很长时间?”
- “互动事件处理脚本代码是否延迟了启动?If so, what else was happening on the main thread at that time?"
- "互动是否导致大量渲染工作延迟渲染下一帧?"
下表列出了您可以从该库中获取的一些基本归因数据,这些数据有助于您大致了解导致网站互动缓慢的一些原因:
attribution 对象键
|
数据 |
---|---|
interactionTarget
|
指向生成网页 INP 值的元素的 CSS 选择器,例如 button#save 。
|
interactionType
|
互动类型,包括点击、点按或键盘输入。 |
inputDelay *
|
互动的输入延迟。 |
processingDuration *
|
从第一个事件监听器开始运行以响应用户互动,到所有事件监听器处理完成所用的时间。 |
presentationDelay *
|
互动的呈现延迟,从事件处理脚本完成到绘制下一个帧之间的时间。 |
longAnimationFrameEntries *
|
与互动相关联的 LoAF 中的条目。如需了解详情,请参阅下文。 |
从 Web Vitals 库 4 版开始,您可以通过 INP 阶段细分数据(输入延迟、处理时长和呈现延迟)以及 Long Animation Frames API (LoAF) 获得更深入的互动问题分析。
Long Animation Frames API (LoAF)
使用现场数据调试互动是一项具有挑战性的任务。然而,有了 LoAF 的数据,您就可以更好地了解导致互动缓慢的原因,因为 LoAF 提供了大量的详细时间安排和其他数据,可供您用来查明精确的原因,更重要的是,问题根源就在您网站的代码中。
web-vitals 库的归因 build 在 attribution
对象的 longAnimationFrameEntries
键下公开了一组 LoAF 条目。下表列出了您可以在每个 LoAF 条目中找到的一些关键数据:
LoAF 条目对象键 | 数据 |
---|---|
duration
|
较长的动画帧的时长,直到布局完成为止(不包括绘制和合成)。 |
blockingDuration
|
在帧内,由于任务耗时较长,浏览器无法快速响应的总时间。此阻塞时间可能包括运行 JavaScript 的长任务,以及帧中的任何后续长渲染任务。 |
firstUIEventTimestamp
|
事件在帧期间加入队列时的时间戳。有助于确定互动开始时的输入延迟。 |
startTime
|
帧的开始时间戳。 |
renderStart
|
帧的渲染工作开始的时间。这包括所有 requestAnimationFrame 回调(以及 ResizeObserver 回调,如果适用),但可能在开始任何样式/布局工作之前。
|
styleAndLayoutStart
|
当帧中发生样式/布局工作时。在计算其他可用时间戳时,有助于确定样式/布局工作时长。 |
scripts
|
一个项数组,其中包含有助于提升网页 INP 的脚本归因信息。 |
所有这些信息都可以让您深入了解导致互动速度缓慢的原因,但 LoAF 条目显示的 scripts
数组特别值得关注:
脚本归因对象键 | 数据 |
---|---|
invoker
|
调用方。这可能会因下行中所述的调用方类型而异。调用方的示例可以是 'IMG#id.onload' 、'Window.requestAnimationFrame' 或 'Response.json.then' 等值。 |
invokerType
|
调用方的类型。可以是 'user-callback' 、'event-listener' 、'resolve-promise' 、'reject-promise' 、'classic-script' 或 'module-script' 。 |
sourceURL
|
长动画帧的来源脚本的网址。 |
sourceCharPosition
|
脚本中由 sourceURL 标识的字符位置。
|
sourceFunctionName
|
所识别脚本中的函数名称。 |
此数组中的每个条目都包含此表中显示的数据,可让您了解导致互动缓慢的脚本以及原因。
衡量和找出互动缓慢的常见原因
为了让您了解如何使用这些信息,本指南将详细介绍如何使用 web-vitals
库中显示的 LoAF 数据来确定导致互动缓慢的一些原因。
处理用时较长
互动的处理时长是指互动所注册的事件处理程序回调运行完毕所需的时间,以及在此期间可能发生的任何其他情况所需的时间。web-vitals 库会显示较长的处理时长:
import {onINP} from 'web-vitals/attribution';
onINP(({name, value, attribution}) => {
const {processingDuration} = attribution; // 512.5
});
您可能会认为互动速度缓慢的主要原因是事件处理脚本代码运行时间过长,但事实并非如此!确认问题所在后,您可以使用 LoAF 数据进行深入分析:
import {onINP} from 'web-vitals/attribution';
onINP(({name, value, attribution}) => {
const {processingDuration} = attribution; // 512.5
// Get the longest script from LoAF covering `processingDuration`:
const loaf = attribution.longAnimationFrameEntries.at(-1);
const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];
if (script) {
// Get attribution for the long-running event handler:
const {invokerType} = script; // 'event-listener'
const {invoker} = script; // 'BUTTON#update.onclick'
const {sourceURL} = script; // 'https://example.com/app.js'
const {sourceCharPosition} = script; // 83
const {sourceFunctionName} = script; // 'update'
}
});
如上方代码段所示,您可以使用 LoAF 数据来跟踪导致互动处理时长值较高的精确原因,包括:
- 元素及其已注册的事件监听器。
- 包含长时间运行的事件处理脚本代码的脚本文件及其字符位置。
- 函数的名称。
这类数据非常有价值。您无需再费力找出究竟是哪个互动(或者它的哪个事件处理脚本)导致处理时长值偏高。此外,由于第三方脚本通常可以注册自己的事件处理脚本,因此您可以确定是您的代码造成了问题!对于您可以控制的代码,您需要考虑优化长时间运行的任务。
输入延迟过长
虽然长时间运行的事件处理程序很常见,但还有其他互动部分需要考虑。其中一部分发生在处理时长之前,称为输入延迟。这是从用户发起互动到其事件处理脚本回调开始运行所用的时间,并且发生在主线程正在处理其他任务时。web-vitals 库的归因 build 可以告诉您互动输入延迟的时长:
import {onINP} from 'web-vitals/attribution';
onINP(({name, value, attribution}) => {
const {inputDelay} = attribution; // 125.59439536
});
如果您发现某些互动存在较长的输入延迟,则需要找出导致输入延迟时间过长的原因。这通常归结为互动是在页面加载时发生还是在加载后发生。
是在网页加载过程中吗?
在网页加载时,主线程通常最繁忙。在此期间,系统会将各种任务加入队列并进行处理。如果用户在所有这些工作进行时尝试与网页互动,则可能会延迟互动。加载大量 JavaScript 的网页可能会启动编译和评估脚本的工作,以及执行用于让网页为用户互动做好准备的函数。如果用户正好在此类活动发生时进行互动,这项工作可能会妨碍用户进行互动,您可以找出网站用户是否属于这种情况:
import {onINP} from 'web-vitals/attribution';
onINP(({name, value, attribution}) => {
const {inputDelay} = attribution; // 125.59439536
// Get the longest script from the first LoAF entry:
const loaf = attribution.longAnimationFrameEntries[0];
const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];
if (script) {
// Invoker types can describe if script eval blocked the main thread:
const {invokerType} = script; // 'classic-script' | 'module-script'
const {sourceLocation} = script; // 'https://example.com/app.js'
}
});
如果您在现场记录此类数据,并发现输入延迟时间较长且调用方类型为 'classic-script'
或 'module-script'
,则可以合理地认为您网站上的脚本需要很长时间才能评估,并且阻塞主线程的时间足以延迟互动。您可以通过将脚本拆分成更小的软件包、推迟最初未使用的代码以便稍后加载,以及审核您的网站以查找可以完全移除的未使用的代码,来缩短此阻塞时间。
是在网页加载后出现的吗?
虽然输入延迟通常会在网页加载期间发生,但也有可能在网页加载后发生,原因完全不同。网页加载后出现输入延迟的常见原因可能是由于之前的 setInterval
调用而定期运行的代码,甚至是之前加入队列以运行且仍在处理的事件回调。
import {onINP} from 'web-vitals/attribution';
onINP(({name, value, attribution}) => {
const {inputDelay} = attribution; // 125.59439536
// Get the longest script from the first LoAF entry:
const loaf = attribution.longAnimationFrameEntries[0];
const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];
if (script) {
const {invokerType} = script; // 'user-callback'
const {sourceURL} = script; // 'https://example.com/app.js'
const {sourceCharPosition} = script; // 83
const {sourceFunctionName} = script; // 'update'
}
});
与排查处理时长值过高的问题一样,如果输入延迟时间因前面提到的原因而过高,您将获得详细的脚本归因数据。不同之处在于,调用方类型会根据延迟互动的工作性质而变化:
'user-callback'
表示阻塞任务来自setInterval
、setTimeout
甚至requestAnimationFrame
。'event-listener'
表示阻塞任务来自已加入队列且仍在处理的早期输入。'resolve-promise'
和'reject-promise'
表示阻塞任务来自于某些异步工作,这些工作之前已启动,并在用户尝试与网页互动时解决或拒绝,导致互动延迟。
在任何情况下,通过脚本归因数据,您都可以了解从何处开始查找,以及输入延迟是由于您自己的代码还是第三方脚本造成的。
呈现延迟时间过长
呈现延迟是互动的最后一公里,从互动事件处理脚本完成开始,到绘制下一个帧结束。当事件处理程序中的工作因互动而更改界面的视觉状态时,就会发生这些问题。与处理时长和输入延迟一样,web-vitals 库可以告诉您互动呈现延迟的时长:
import {onINP} from 'web-vitals/attribution';
onINP(({name, value, attribution}) => {
const {presentationDelay} = attribution; // 113.32307691
});
如果您记录了这些数据,并发现导致您网站 INP 的互动存在较长的呈现延迟,原因可能不尽相同,但以下是需要注意的几个原因。
样式和布局工作费用高昂
呈现延迟过长的样式重新计算和布局工作可能代价高昂,原因有很多,包括复杂的 CSS 选择器和大型 DOM 大小。您可以使用 web-vitals 库中显示的 LoAF 时间来衡量此工作所花费的时间:
import {onINP} from 'web-vitals/attribution';
onINP(({name, value, attribution}) => {
const {presentationDelay} = attribution; // 113.32307691
// Get the longest script from the last LoAF entry:
const loaf = attribution.longAnimationFrameEntries.at(-1);
const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];
// Get necessary timings:
const {startTime} = loaf; // 2120.5
const {duration} = loaf; // 1002
// Figure out the ending timestamp of the frame (approximate):
const endTime = startTime + duration; // 3122.5
// Get the start timestamp of the frame's style/layout work:
const {styleAndLayoutStart} = loaf; // 3011.17692309
// Calculate the total style/layout duration:
const styleLayoutDuration = endTime - styleAndLayoutStart; // 111.32307691
if (script) {
// Get attribution for the event handler that triggered
// the long-running style and layout operation:
const {invokerType} = script; // 'event-listener'
const {invoker} = script; // 'BUTTON#update.onclick'
const {sourceURL} = script; // 'https://example.com/app.js'
const {sourceCharPosition} = script; // 83
const {sourceFunctionName} = script; // 'update'
}
});
LoAF 不会告诉您样式和布局工作在帧中的时长,但会告诉您它何时开始。有了此开始时间戳,您就可以使用 LoAF 中的其他数据来计算相应工作的确切时长,方法是确定帧的结束时间,然后从中减去样式和布局工作的开始时间戳。
长时间运行的 requestAnimationFrame
回调
导致呈现延迟时间过长的一个可能原因是 requestAnimationFrame
回调中执行的工作量过多。此回调的内容会在事件处理程序运行完毕后执行,但在样式重新计算和布局工作之前。
如果这些回调中执行的工作很复杂,则可能需要很长时间才能完成。如果您怀疑呈现延迟时间值较高的原因是您使用 requestAnimationFrame
执行的工作,则可以使用 Web Vitals 库显示的 LoAF 数据来识别以下场景:
onINP(({name, value, attribution}) => {
const {presentationDelay} = attribution; // 543.1999999880791
// Get the longest script from the last LoAF entry:
const loaf = attribution.longAnimationFrameEntries.at(-1);
const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];
// Get the render start time and when style and layout began:
const {renderStart} = loaf; // 2489
const {styleAndLayoutStart} = loaf; // 2989.5999999940395
// Calculate the `requestAnimationFrame` callback's duration:
const rafDuration = styleAndLayoutStart - renderStart; // 500.59999999403954
if (script) {
// Get attribution for the event handler that triggered
// the long-running requestAnimationFrame callback:
const {invokerType} = script; // 'user-callback'
const {invoker} = script; // 'FrameRequestCallback'
const {sourceURL} = script; // 'https://example.com/app.js'
const {sourceCharPosition} = script; // 83
const {sourceFunctionName} = script; // 'update'
}
});
如果您发现呈现延迟时间的大部分时间都花在了 requestAnimationFrame
回调中,请确保您在这些回调中执行的工作仅限于执行可实际更新界面的工作。不涉及 DOM 或更新样式的任何其他工作都会不必要地延迟绘制下一帧,因此请务必小心!
总结
如需了解哪些互动会对现场的实际用户造成问题,实测数据是最佳的信息来源。通过使用 Web Vitals JavaScript 库(或 RUM 提供程序)等现场数据收集工具,您可以更确切地了解哪些互动存在问题,然后在实验室中重现存在问题的互动,并着手进行修复。
主打图片来自 Unsplash,摄影师:Federico Respini。