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

了解如何在您网站的实际应用数据中查找缓慢的互动,以便您可以找到改进“首次内容互动延迟”的机会。

实际应用数据是告知您真实用户如何体验您网站的数据。它揭示了您仅靠实验室数据无法发现的问题。就首次内容互动延迟 (INP) 而言,实际应用数据对于识别缓慢的互动至关重要,并为帮助您修复这些互动提供重要线索。

在本指南中,您将学习如何使用来自 Chrome 用户体验报告 (CrUX) 的实际应用数据快速评估您网站的 INP,以查看您的网站是否存在 INP 问题。随后,您将学习如何使用 web-vitals JavaScript 库的归因版本,以及它从 长动画帧 API (LoAF) 中提供的新见解,来收集和解释您网站上缓慢互动的实际应用数据。

首先使用 CrUX 评估您网站的 INP

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

CrUX 数据在许多不同的领域中呈现,这取决于您要查找的信息范围。CrUX 可以为以下内容提供关于 INP 和其他 Core Web Vitals 的数据:

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

作为起点,您可以在 PageSpeed Insights 中输入您网站的网址。输入网址后,如果可用,将显示其字段数据,包括 INP 等多项指标。您也可以使用切换开关来查看您的移动设备和桌面设备尺寸的 INP 值。

Field data as shown by CrUX in PageSpeed Insights, showing LCP, INP, CLS at the three Core Web Vitals, and TTFB, FCP as diagnostic metrics, and FID as a deprecated Core Web Vital metric.
在 PageSpeed Insights 中看到的 CrUX 数据读数。在此示例中,给定网页的 INP 需要改进。

此数据很有用,因为它会告诉您是否存在问题。但是,CrUX 无法告诉您什么导致了问题。有许多真实用户监控 (RUM) 解决方案可用,它们将帮助您从您网站的用户那里收集您自己的实际应用数据,以帮助您回答这个问题,其中一个选择是使用 web-vitals JavaScript 库自行收集实际应用数据。

使用 web-vitals JavaScript 库收集实际应用数据

web-vitals JavaScript 库是您可以加载到您网站上的脚本,用于从您网站的用户那里收集实际应用数据。您可以使用它来记录许多指标,包括支持 INP 的浏览器中的 INP。

浏览器支持

  • Chrome: 96.
  • Edge: 96.
  • Firefox:不支持。
  • Safari:不支持。

来源

web-vitals 库的标准版本可用于从实际应用中的用户获取基本的 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 库公开了您可以从实际应用中的用户那里获得的额外数据,以帮助您更好地排除影响您网站 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
});
How console logs from the web-vitals library appears. The console in this example shows the name of the metric (INP), the INP value (56), where that value resides within the INP thresholds (good), and the various bits of information shown in the attribution object, including entries from The Long Animation Frames API.
web-vitals 库中的数据在控制台中的显示方式。

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

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

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

attribution 对象键 数据
interactionTarget 指向生成页面 INP 值的元素的 CSS 选择器,例如 button#save
interactionType 互动的类型,来自点击、触摸或键盘输入。
inputDelay* 互动的输入延迟
processingDuration* 从第一个事件侦听器开始运行以响应用户互动到所有事件侦听器处理完成的时间。
presentationDelay* 互动的呈现延迟,它发生在事件处理程序完成到下一个帧绘制的时间之间。
longAnimationFrameEntries* 与互动关联的 LoAF 条目。有关其他信息,请参阅下文。
*版本 4 中的新增功能

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

长动画帧 API (LoAF)

浏览器支持

  • Chrome: 123.
  • Edge: 123.
  • Firefox:不支持。
  • Safari:不支持。

来源

使用实际应用数据调试互动是一项具有挑战性的任务。但是,借助来自 LoAF 的数据,现在可以更好地了解缓慢互动背后的原因,因为 LoAF 公开了许多详细的计时和其他数据,您可以用来查明精确的原因,更重要的是,问题来源在您网站代码中的位置。

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

LoAF 条目对象键 数据
duration 长动画帧的持续时间,直到布局完成,但不包括绘制和合成。
blockingDuration 由于长时间运行的任务,浏览器无法快速响应的帧中的总时间量。此阻塞时间可以包括运行 JavaScript 的长时间运行的任务,以及帧中任何后续的长时间运行的渲染任务。
firstUIEventTimestamp 事件在帧中排队时的时间戳。对于找出互动输入延迟的开始时间很有用。
startTime 帧的开始时间戳。
renderStart 帧的渲染工作开始的时间。这包括任何 requestAnimationFrame 回调(以及ResizeObserver 回调,如果适用),但可能在任何样式/布局工作开始之前。
styleAndLayoutStart 帧中发生样式/布局工作的时间。在计算其他可用时间戳时,可用于找出样式/布局工作的长度。
scripts 包含有助于页面 INP 的脚本归因信息的项目数组。
A visualization of a long animation frame according to the LoAF model.
根据 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 长动画帧起源的脚本的 URL。
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 库的归因版本可以告诉您互动的输入延迟长度

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