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

了解如何在网站的现场数据中查找互动缓慢的问题,以便找出机会来提高 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 值。

PageSpeed Insights 中 CrUX 显示的实测数据,其中显示了三个 Core Web Vitals 指标(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 的浏览器中的 INP。

浏览器支持

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

来源

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 还提供了大量数据,可帮助您了解互动缓慢的原因,包括您应重点关注互动过程的哪个部分。它可以帮助您解答诸如以下重要问题:

  • “用户在网页加载期间是否与网页互动?”
  • “互动事件处理脚本是否运行了很长时间?”
  • “互动事件处理脚本代码是否延迟了启动?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 中的条目。如需了解详情,请参阅下文。
*版本 4 中的新功能

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

Long Animation Frames API (LoAF)

浏览器支持

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

来源

使用现场数据调试互动是一项具有挑战性的任务。不过,借助 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' 表示阻塞任务来自 setIntervalsetTimeoutrequestAnimationFrame
  • '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