查找实际应用中的缓慢互动情况

了解如何在网站的现场数据中找出缓慢的互动,以便找到改进 Interaction to Next Paint 的机会。

现场数据是指反映实际用户在您网站上的体验的数据。它能找出仅通过实验室数据无法发现的问题。就 Interaction to Next Paint (INP) 而言,现场数据对于识别缓慢的互动至关重要,并可提供重要线索来帮助您解决这些问题。

在本指南中,您将了解如何使用 Chrome 用户体验报告 (CrUX) 中的实测数据快速评估网站的 INP,以了解网站是否存在 INP 问题。随后,您将学习如何使用web-vitals JavaScript 库的归因 build,以及它通过 长动画帧 API (LoAF) 提供的全新数据洞见,来收集和解读网站上慢速互动的现场数据。

首先使用 CrUX 评估网站的 INP

如果您没有从网站用户那里收集字段数据,CrUX 可能是一个不错的起点。CrUX 会从选择发送遥测数据的真实 Chrome 用户那里收集实测数据。

CrUX 数据会显示在多个不同的区域,具体取决于您要查找的信息范围。CrUX 可提供以下方面的 INP 和其他 Core Web Vitals 数据:

  • 使用 PageSpeed Insights 的各个网页和整个源。
  • 网页类型。例如,许多电子商务网站都有商品详情页面和商品详情页面类型。您可以在 Search Console 中获取各种网页类型的 CrUX 数据。

首先,您可以在 PageSpeed Insights 中输入网站的网址。输入网址后,系统会显示该网址的字段数据(如果有),涵盖多个指标,包括 INP。您还可以使用切换开关查看移动设备和桌面设备维度的 INP 值。

PageSpeed Insights 中 CrUX 显示的实测数据,其中 LCP、INP、CLS 为三项 Core Web Vitals 指标,TTFB、FCP 为诊断指标,FID 为已弃用的 Core Web Vitals 指标。
PageSpeed Insights 中显示的 CrUX 数据读数。在此示例中,给定网页的 INP 需要改进。

这些数据非常有用,因为它们可以告诉您是否存在问题。不过,CrUX 无法告诉您什么导致了问题。市面上有很多真实用户监控 (RUM) 解决方案,可帮助您从网站用户那里收集自己的现场数据,从而回答这个问题。其中一种方法是使用 web-vitals JavaScript 库自行收集这些现场数据。

使用 web-vitals JavaScript 库收集字段数据

web-vitals JavaScript 库是一个脚本,您可以在网站上加载该脚本,以从网站用户那里收集字段数据。您可以使用它来记录多项指标,包括在支持 INP 的浏览器中记录 INP。

Browser Support

  • Chrome: 96.
  • Edge: 96.
  • Firefox: 144.
  • Safari: 26.2.

Source

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 库的归因 build 的用武之地。

利用 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
});
web-vitals 库中的控制台日志的显示方式。此示例中的控制台显示了指标名称 (INP)、INP 值 (56)、该值在 INP 阈值中的位置(良好),以及归因对象中显示的各种信息,包括来自长动画帧 API 的条目。
web-vitals 库中的数据在控制台中的显示方式。

除了网页的 INP 本身之外,归因 build 还提供了大量数据,可帮助您了解互动缓慢的原因,包括您应该关注互动的哪个部分。它可帮助您解答诸如以下重要问题:

  • “用户是否在网页加载期间与其进行了互动?”
  • “互动的事件处理脚本是否运行了很长时间?”
  • “互动事件处理脚本代码是否延迟启动?如果是,当时主线程上还发生了什么?”
  • “互动是否导致了大量渲染工作,从而延迟了下一帧的绘制?”

下表显示了您可以从该库中获取的一些基本归因数据,这些数据可帮助您找出网站上互动缓慢的一些大致原因:

attribution 对象键 数据
interactionTarget 指向产生网页 INP 值的元素的 CSS 选择器,例如 button#save。
interactionType 互动类型,包括点击、点按或键盘输入。
inputDelay* 互动的输入延迟时间。
processingDuration* 从第一个事件监听器开始运行以响应用户互动,到所有事件监听器处理完毕所用的时间。
presentationDelay* 互动的呈现延迟时间,从事件处理脚本完成到下一帧绘制完成。
longAnimationFrameEntries* 与互动关联的 LoAF 中的条目。如需了解更多信息,请参阅下一部分。
*第 4 版中的新功能

从 web-vitals 库第 4 版开始,您可以通过该库提供的 INP 阶段细分数据(输入延迟、处理时长和呈现延迟)以及 Long Animation Frames API (LoAF),更深入地了解存在问题的互动。

Long Animation Frames API (LoAF)

Browser Support

  • Chrome: 123.
  • Edge: 123.
  • Firefox: not supported.
  • Safari: not supported.

Source

使用现场数据调试互动是一项具有挑战性的任务。不过,借助 LoAF 提供的数据,现在可以更好地了解互动缓慢背后的原因,因为 LoAF 会提供许多详细的时间和其他数据,您可以使用这些数据来查明确切原因,更重要的是,还可以查明问题在网站代码中的来源。

web-vitals 库的归因 build 会在 attribution 对象的 longAnimationFrameEntries 键下公开一个 LoAF 条目数组。下表列出了您可以在每个 LoAF 条目中找到的一些关键数据:

LoAF 条目对象键 数据
duration 长动画帧的时长,截至布局完成,但不包括绘制和合成。
blockingDuration 在相应帧中,浏览器因长任务而无法快速响应的总时长。此阻塞时间可能包括运行 JavaScript 的长任务,以及帧中的任何后续长渲染任务。
firstUIEventTimestamp 事件在帧期间排队的时间戳。可用于确定互动输入延迟的开始时间。
startTime 帧的起始时间戳。
renderStart 帧的渲染工作开始的时间。这包括任何 requestAnimationFrame 回调(以及 ResizeObserver 回调,如果适用),但可能在任何样式/布局工作开始之前。
styleAndLayoutStart 帧中发生样式/布局工作的时间。在计算其他可用时间戳时,有助于确定样式/布局工作的时长。
scripts 一个数组,其中包含对网页 INP 有影响的脚本归因信息。
根据 LoAF 模型呈现的长动画帧可视化图表。
根据 LoAF API(减去 blockingDuration)绘制的长动画帧时间图。

所有这些信息都可以让您深入了解导致互动缓慢的原因,但 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.toSorted((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.toSorted((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.toSorted((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.toSorted((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.toSorted((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 提供商)等现场数据收集工具,您可以更确信哪些互动最容易出现问题,然后继续在实验室中重现这些有问题的互动,并着手修复它们。