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

了解如何查找网站实际数据中的缓慢互动,以便找到可用于提高 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 值。

PageSpeed Insights 中的 CrUX 显示的实测数据,其中 LCP、INP、CLS 为三项核心网页指标,TTFB、FCP 为诊断指标,FID 为已弃用的核心网页指标。
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: not supported.

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

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

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

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

attribution 对象键 数据
interactionTarget 一个 CSS 选择器,用于指向生成网页 INP 值的元素,例如 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' 表示阻塞任务来自 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.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 提供商),您可以更确信哪些互动最成问题,然后继续在实验室内重现问题互动,接着着手修复这些问题。

Unsplash 上的主打图片,由 Federico Respini 提供。