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

了解如何在网站的实测数据中查找缓慢互动,以便寻找改进其 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 作为已弃用的核心网页指标指标。
读取 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 库的归因版本会显示您可以从现场用户那里获取的其他数据,以帮助您更好地排查影响网站 INP 的问题互动问题。您可以通过库的 onINP() 方法中显示的 attribution 对象访问此数据:

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, rating, attribution}) => {
  console.log(name);         // 'INP'
  console.log(value);        // 512
  console.log(rating);       // 'poor'
  console.dir(attribution);  // Attribution data
});
来自 web-Vitals 库的控制台日志的显示方式。此示例中的控制台显示了指标的名称 (INP)、INP 值 (56),其中该值在 INP 阈值范围内(良好),以及归因对象中显示的各种信息位,包括来自 Long Animation Frame API 的条目。
Web-Vitals 库中的数据在控制台中的显示方式。

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

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

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

attribution 对象键 数据
interactionTarget 指向生成网页 INP 值的元素(例如 button#save)的 CSS 选择器。
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 库的归因版本可以告诉您互动的输入延迟长度:

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 提供商)等现场数据收集工具,您可以更有信心地了解哪些互动最有问题,然后在实验室中重现有问题的互动,并着手解决这些问题。

主打图片来自 Unsplash 用户,由 Federico Respini 提供。