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

了解如何在网站的实测数据中找出缓慢互动,以便寻找改进其 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 和其他核心网页指标的数据,以便:

  • 使用 PageSpeed Insights 处理具体网页和整个源。
  • 网页类型。例如,许多电子商务网站都有“商品详情”和“商品详情页”这两种类型。您可以在 Search Console 中获取独特网页类型的 CrUX 数据。

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

CrUX 在 PageSpeed Insights 中显示的实测数据,在三个核心网页指标中显示 LCP、INP 和 CLS,并将 TTFB、FCP 作为诊断指标,以及 FID 作为已弃用的 Core Web Vitals 指标。
PageSpeed Insights 中显示的 CrUX 数据读出。在此示例中,指定网页的 INP 需要改进。

这些数据非常有用,因为它可以告诉您是否出现了问题。但是,CrUX 无法做到的是告诉您导致问题的原因。许多真实用户监控 (RUM) 解决方案可帮助您从网站用户那里收集自己的现场数据,以帮助您回答这个问题。一种方法是使用 web-Vitals JavaScript 库自行收集现场数据。

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

web-vitals JavaScript 库是一个可在您的网站上加载的脚本,用于收集网站用户的现场数据。您可以使用它来记录多项指标,包括支持此功能的浏览器中的 INP。

浏览器支持

  • 96
  • 96
  • x
  • x

来源

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 库的归因版本

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 阈值(良好)以及归因对象中显示的各种信息,包括来自 Long Animation Frame API 的条目。
web-vitals 库中的数据在控制台中的显示方式。

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

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

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

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

从 web-vitals 库版本 4 开始,您可以通过它提供的 INP 阶段细分数据(输入延迟、处理时长和呈现延迟)和 Long Animation Frame API (LoAF) 来更深入地探究有问题的互动。

Long Animation Frame API (LoAF)

浏览器支持

  • 123
  • 123
  • x
  • x

来源

使用现场数据调试互动是一项具有挑战性的任务。然而,有了 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.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' 表示阻塞任务来自 setIntervalsetTimeout 甚至 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 提供商)等现场数据收集工具,您可以更确信哪些互动最存在问题,然后继续在实验室中重现有问题的互动,然后着手解决这些问题。

主打图片来自 Unstone,由 Federico Respini 提供。