了解如何使用调试信息对性能数据进行归因,以帮助您通过分析识别和修复真实用户问题
Google 提供了两类工具来衡量和调试性能:
- 实验室工具:Lighthouse 等工具。在这些工具中,您的页面会在可以模拟各种条件(例如,慢速网络和低端移动设备)的模拟环境中加载。
- 现场工具:Chrome 用户体验报告 (CrUX) 等工具,该工具根据来自 Chrome 的汇总真实用户数据生成。(请注意,PageSpeed Insights 和 Search Console 等工具报告的字段数据来自 CrUX 数据。)
虽然现场工具可以提供更准确的数据(即实际代表真实用户体验的数据),但实验室工具通常可以更好地帮助您识别和解决问题。
CrUX 数据更能代表网页的实际性能,但了解 CrUX 得分不太可能帮助您找出如何提升性能。
另一方面,Lighthouse 将发现问题并针对如何改进提出具体建议。不过,Lighthouse 只会针对其在网页加载时发现的性能问题提供建议。它不会检测仅因用户互动(例如滚动或点击页面上的按钮)而出现的问题。
这带来了一个重要的问题:如何从实际用户的那里捕获核心网页指标的调试信息或其他性能指标?
这篇博文详细介绍了您可以使用哪些 API 为当前的每个核心网页指标收集额外的调试信息,并就如何在现有分析工具中捕获这些数据为您提供一些思路。
用于归因和调试的 API
CLS
在所有核心网页指标中,CLS 可能是在现场收集调试信息的最重要指标。CLS 是在网页的整个生命周期内衡量的,因此用户与网页互动的方式(他们滚动的距离、点击的内容等)会对是否发生布局偏移以及哪些元素在发生偏移方面产生重大影响。
参考 PageSpeed Insights 的以下报告:
实验室 (Lighthouse) 报告的 CLS 值与现场的 CLS 值(CrUX 数据)存在很大差异,如果您认为页面可能包含在 Lighthouse 中测试时可能并未使用的许多互动内容,这种做法是合理的。
但即使您了解用户互动会影响字段数据,您仍然需要知道页面上的哪些元素发生了变化,从而得出第 75 百分位的得分为 0.3。
LayoutShiftAttribution 接口可以实现这一目的。
获取布局偏移归因
LayoutShiftAttribution 接口会在 Layout Instability API 发出的每个 layout-shift
条目上公开。
如需详细了解这两个接口,请参阅调试布局偏移。在这篇博文中,您需要知道的主要一点是,作为开发者,您可以观察页面上发生的每次布局偏移,以及哪些元素发生了偏移。
下面这些示例代码记录了每次布局偏移以及偏移的元素:
new PerformanceObserver((list) => {
for (const {value, startTime, sources} of list.getEntries()) {
// Log the shift amount and other entry info.
console.log('Layout shift:', {value, startTime});
if (sources) {
for (const {node, curRect, prevRect} of sources) {
// Log the elements that shifted.
console.log(' Shift source:', node, {curRect, prevRect});
}
}
}
}).observe({type: 'layout-shift', buffered: true});
对于发生的每一次布局偏移,衡量数据并将其发送到您的分析工具可能不太现实;但是,通过监控所有偏移,您可以跟踪最糟糕的偏移并仅报告相关信息。
我们的目标不是找出并解决每位用户发生的每一次布局偏移,而是要找出影响最多用户且因此对页面的 CLS 影响最大的变化(位于第 75 百分位)。
此外,您不需要在每次发生偏移时计算最大的源元素,只有在准备好将 CLS 值发送到分析工具时才需要计算。
以下代码接受对 CLS 做出贡献的 layout-shift
条目的列表,并从最大偏移中返回最大的源元素:
function getCLSDebugTarget(entries) {
const largestEntry = entries.reduce((a, b) => {
return a && a.value > b.value ? a : b;
});
if (largestEntry && largestEntry.sources && largestEntry.sources.length) {
const largestSource = largestEntry.sources.reduce((a, b) => {
return a.node && a.previousRect.width * a.previousRect.height >
b.previousRect.width * b.previousRect.height ? a : b;
});
if (largestSource) {
return largestSource.node;
}
}
}
一旦确定了导致变化最大的元素,您就可以向分析工具报告这一结果。
对于给定页面,对 CLS 影响最大的元素可能因用户而异,但如果您汇总所有用户的这些元素,就能够生成影响最多用户数的转变元素列表。
在确定并修复这些元素变化的根本原因后,分析代码将开始报告网页变化的“最差”变化。最终,所有报告的偏移都很小,足以让您的网页很好地保持在“良好”阈值 (0.1) 的范围内!
与最大偏移源元素一起捕获的其他一些元数据可能很有用,包括:
- 变化幅度最大的时间
- 发生最大变化时的网址路径(适用于动态更新网址的网站,如单页应用)。
LCP
若要在该字段中调试 LCP,您需要的主要信息是哪个特定元素是该特定网页加载的最大元素(LCP 候选元素)。
请注意,对于不同的用户,LCP 候选元素完全可能有所不同(事实上很常见),即使对于同一个网页也是如此。
以下是可能导致此问题的原因:
- 用户设备具有不同的屏幕分辨率,这会导致不同的页面布局,进而在视口内显示不同的元素。
- 用户不一定会加载滚动到最顶部的页面。通常,链接会包含片段标识符,甚至是文本片段,这意味着您的网页可能会在页面上的任何滚动位置加载并显示。
- 内容可能是针对当前用户进行个性化处理的,因此 LCP 候选元素可能会因用户而异。
这意味着,您无法假设哪个元素或一组元素将是特定网页的最常见 LCP 候选元素。您必须基于真实用户行为对其进行衡量。
确定 LCP 候选元素
如需确定 JavaScript 中的 LCP 候选元素,您可以使用 Largest Contentful Paint API,后者即用于确定 LCP 时间值的 API。
观察 largest-contentful-paint
条目时,您可以通过查看最后一个条目的 element
属性来确定当前的 LCP 候选元素:
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP element:', lastEntry.element);
}).observe({type: 'largest-contentful-paint', buffered: true});
知道 LCP 候选元素后,您可以将其与指标值一起发送到您的分析工具。与 CLS 一样,这可以帮助您确定哪些元素最有必要先进行优化。
除了 LCP 候选元素之外,衡量 LCP 子部分时间可能也很有用,这对于确定与您的网站相关的特定优化步骤非常有用。
FID
为了在现场调试 FID,请务必注意,FID 仅测量整体 First Input 事件延迟的延迟部分。这意味着,用户交互的内容并不像交互时主线程上发生的其他事件那么重要。
例如,许多支持服务器端呈现 (SSR) 的 JavaScript 应用都会提供静态 HTML,这种 HTML 可以在界面与用户输入内容互动之前(即,使内容互动所需的 JavaScript 完成加载之前)呈现到屏幕上。
对于这些类型的应用,了解首次输入是发生在水合之前还是之后非常重要。如果发现有很多人在水合要求完成之前尝试与网页互动,请考虑以停用或加载状态呈现网页,而不是使其看起来像互动一样。
如果您的应用框架公开了水合时间戳,您可以将其与 first-input
条目的时间戳进行比较,以确定首次输入是发生在水合之前还是之后。如果您的框架不显示该时间戳,或者根本不使用“Hydration”,那么另一个有用的信号可能是输入是在 JavaScript 完成加载之前还是之后发生。
DOMContentLoaded
事件会在网页的 HTML 完全加载并解析后触发,包括等待加载任何同步脚本、延迟脚本或模块脚本(包括所有静态导入的模块)。因此,您可以使用该事件的时间,并将其与发生 FID 的时间进行比较。
以下代码会观察 first-input
条目,并记录首次输入是否发生在 DOMContentLoaded
事件结束之前:
new PerformanceObserver((list) => {
const fidEntry = list.getEntries()[0];
const navEntry = performance.getEntriesByType('navigation')[0];
const wasFIDBeforeDCL =
fidEntry.startTime < navEntry.domContentLoadedEventStart;
console.log('FID occurred before DOMContentLoaded:', wasFIDBeforeDCL);
}).observe({type: 'first-input', buffered: true});
识别 FID 目标元素和事件类型
其他可能有用的调试信号包括互动过的元素及其互动类型(例如 mousedown
、keydown
、pointerdown
)。虽然与元素本身的互动不影响 FID(请注意,FID 只是总事件延迟的延迟部分),但了解用户与哪些元素互动可能有助于确定如何以最佳方式改进 FID。
例如,如果用户的绝大多数首次互动都是与特定元素进行的,请考虑在 HTML 中内嵌该元素所需的 JavaScript 代码,并延迟加载其余元素。
如需获取与第一个输入事件相关联的互动类型和元素,您可以引用 first-input
条目的 target
和 name
属性:
new PerformanceObserver((list) => {
const fidEntry = list.getEntries()[0];
console.log('FID target element:', fidEntry.target);
console.log('FID interaction type:', fidEntry.name);
}).observe({type: 'first-input', buffered: true});
INP
INP 与 FID 非常相似,因为在现场捕获的最有用的信息位是:
- 用户与哪个元素进行了互动
- 为什么选择互动类型
- 互动发生的时间
与 FID 一样,导致交互缓慢的主要原因是主线程被阻塞,在 JavaScript 加载时,这种情况很常见。了解大多数互动速度缓慢是否发生在网页加载期间,有助于确定需要采取哪些措施来解决问题。
与 FID 不同,INP 指标考虑的是互动的完整延迟时间,包括运行任何已注册的事件监听器所用的时间,以及在所有事件监听器运行完后绘制下一帧所用的时间。这意味着,对于 INP 而言,了解哪些目标元素往往会导致互动缓慢,以及这些是哪些类型的互动。
由于 INP 和 FID 均基于 Event Timing API,因此在 JavaScript 中确定此信息的方式与上一个示例非常类似。以下代码会记录目标元素和 INP 条目的时间(相对于 DOMContentLoaded
)。
function logINPDebugInfo(inpEntry) {
console.log('INP target element:', inpEntry.target);
console.log('INP interaction type:', inpEntry.name);
const navEntry = performance.getEntriesByType('navigation')[0];
const wasINPBeforeDCL =
inpEntry.startTime < navEntry.domContentLoadedEventStart;
console.log('INP occurred before DCL:', wasINPBeforeDCL);
}
请注意,此代码未显示如何确定哪个 event
条目是 INP 条目,因为此逻辑更为复杂。不过,以下部分介绍了如何使用 web-vitals JavaScript 库获取此信息。
使用 web- Vitals JavaScript 库
上文提供了一些常规建议和代码示例,用于捕获要包含在发送到分析工具的数据中的调试信息。
从版本 3 开始,web-vitals JavaScript 库增加了一个归因 build 来显示所有这些信息,此外还添加了一些其他信号。
以下代码示例展示了如何设置包含调试字符串的其他事件参数(或自定义维度),以帮助确定性能问题的根本原因。
import {onCLS, onFID, onINP, onLCP} from 'web-vitals/attribution';
function sendToGoogleAnalytics({name, value, id, attribution}) {
const eventParams = {
metric_value: value,
metric_id: id,
}
switch (name) {
case 'CLS':
eventParams.debug_target = attribution.largestShiftTarget;
break;
case 'LCP':
eventParams.debug_target = attribution.element;
break;
case 'FID':
case 'INP':
eventParams.debug_target = attribution.eventTarget;
break;
}
// Assumes the global `gtag()` function exists, see:
// https://developers.google.com/analytics/devguides/collection/ga4
gtag('event', name, eventParams);
}
onCLS(sendToGoogleAnalytics);
onLCP(sendToGoogleAnalytics);
onFID(sendToGoogleAnalytics);
onINP(sendToGoogleAnalytics);
此代码是 Google Analytics(分析)专用的,但其大致思路也应适用于其他分析工具。
此代码还展示了如何报告单个调试信号,但可以针对每个指标收集并报告多个不同的信号可能很有用。例如,如需调试 INP,您可能需要收集互动类型、时间以及被互动的元素。web-vitals
归因 build 会公开所有这些信息,如以下示例所示:
import {onCLS, onFID, onINP, onLCP} from 'web-vitals/attribution';
function sendToGoogleAnalytics({name, value, id, attribution}) {
const eventParams = {
metric_value: value,
metric_id: id,
}
switch (name) {
case 'INP':
eventParams.debug_target = attribution.eventTarget;
eventParams.debug_type = attribution.eventType;
eventParams.debug_time = attribution.eventTime;
eventParams.debug_load_state = attribution.loadState;
break;
// Additional metric logic...
}
// Assumes the global `gtag()` function exists, see:
// https://developers.google.com/analytics/devguides/collection/ga4
gtag('event', name, eventParams);
}
onCLS(sendToGoogleAnalytics);
onLCP(sendToGoogleAnalytics);
onFID(sendToGoogleAnalytics);
onINP(sendToGoogleAnalytics);
如需查看公开的调试信号的完整列表,请参阅 Web Vitals 归因文档。
报告数据并直观呈现
开始收集调试信息以及指标值后,下一步就是汇总所有用户的数据,以开始寻找规律和趋势。
如上所述,您不一定需要解决用户遇到的每一个问题,而是需要解决影响最多用户的问题,尤其是在开始时,这些问题也应该是对核心网页指标得分影响最大的问题。
对于 GA4,请参阅关于如何使用 BigQuery 查询和直观呈现数据的专门文章。
摘要
希望这篇博文已经概述了如何使用现有的性能 API 和 web-vitals
库来获取调试信息,以帮助根据现场的真实用户访问来诊断性能。虽然本指南侧重于核心网页指标,但其中的概念也适用于在 JavaScript 中可衡量的任何性能指标的调试。
如果您刚开始衡量性能,并且已经是 Google Analytics(分析)用户,那么网页指标报告工具可能是一个不错的选择,因为它已支持报告核心网页指标的调试信息。
如果您是分析服务供应商,并且想要改进您的产品并为用户提供更多调试信息,请考虑本文中介绍的一些方法,但不要仅限于本文介绍的思路。这篇博文旨在广泛地应用于所有分析工具,但各个分析工具可能(并且应该)可以(并且应该)捕获和报告更多调试信息。
最后,如果您认为由于 API 本身缺少功能或信息而无法调试这些指标,请将反馈发送至 web-vitals-feedback@googlegroups.com。