1. 简介
这是一个互动式 Codelab,旨在帮助您了解如何使用 web-vitals
库衡量 Interaction to Next Paint (INP)。
前提条件
- 具备 HTML 和 JavaScript 开发方面的知识。
- 建议:阅读 web.dev INP 指标文档。
学习内容
- 如何将
web-vitals
库添加到您的网页并使用其归因数据。 - 使用归因数据诊断从何处着手以及如何提升 INP。
所需条件
- 能够从 GitHub 克隆代码并运行 npm 命令的计算机。
- 文本编辑器。
- 较新版本的 Chrome,以便所有互动衡量功能正常运行。
2. 进行设置
获取并运行代码
该代码位于 web-vitals-codelabs
代码库中。
- 在终端中克隆代码库:
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
。 - 进入克隆的目录:
cd web-vitals-codelabs/measuring-inp
。 - 安装依赖项:
npm ci
。 - 启动 Web 服务器:
npm run start
。 - 在浏览器中访问 http://localhost:8080/。
试用页面
此 Codelab 使用 Gastropodicon(一个热门的蜗牛解剖学参考网站)来探索 INP 的潜在问题。
尝试与网页互动,了解哪些互动速度缓慢。
3. 熟悉 Chrome 开发者工具
打开 DevTools:依次选择更多工具 > 开发者工具菜单,右键点击页面并选择检查,或使用快捷键。
在本 Codelab 中,我们将同时使用性能面板和控制台。您可以随时在 DevTools 顶部的标签页中切换这些视图。
- INP 问题最常发生在移动设备上,因此请改用移动设备显示屏模拟。
- 如果您是在桌面设备或笔记本电脑上进行测试,性能可能会比在真实移动设备上明显更好。如需更真实地了解性能,请点击性能面板右上角的齿轮图标,然后选择将 CPU 速度降低 4 倍。
4. 安装 web-vitals
web-vitals
是一个 JavaScript 库,用于衡量用户体验的 Web Vitals 指标。您可以使用该库捕获这些值,然后将其信标到分析端点以供日后分析,以便我们确定互动缓慢的时间和位置。
您可以通过几种不同的方式将库添加到网页中。您在自己的网站上安装该库的方式取决于您管理依赖项的方式、构建流程和其他因素。请务必查看该库的文档,了解所有选项。
本 Codelab 将通过 npm 进行安装并直接加载脚本,以避免深入研究特定的构建流程。
您可以使用以下两个版本的 web-vitals
:
- 如果您想跟踪网页加载时核心网页指标的指标值,则应使用“标准”build。
- “归因”build 会向每个指标添加其他调试信息,以便诊断指标最终获得其值的原因。
为了在此 Codelab 中衡量 INP,我们需要使用归因 build。
运行 npm install -D web-vitals
将 web-vitals
添加到项目的 devDependencies
将 web-vitals
添加到页面:
将脚本的归因版本添加到 index.html
底部,并将结果记录到控制台:
<script type="module">
import {onINP} from './node_modules/web-vitals/dist/web-vitals.attribution.js';
onINP(console.log);
</script>
试试看
打开控制台,然后尝试再次与该网页互动。您在页面中点击时,系统没有记录任何内容!
INP 是在网页的整个生命周期内衡量的,因此默认情况下,web-vitals
不会在用户离开或关闭网页之前报告 INP。这对于发送用于分析等用途的信标来说是理想行为,但对于交互式调试来说则不太理想。
web-vitals
提供了一个用于生成更详细报告的 reportAllChanges
选项。启用此功能后,系统不会报告每一次互动,但每当有互动比之前的任何互动都慢时,系统都会报告该互动。
请尝试将该选项添加到脚本中,然后再次与该页面互动:
<script type="module">
import {onINP} from './node_modules/web-vitals/dist/web-vitals.attribution.js';
onINP(console.log, {reportAllChanges: true});
</script>
刷新页面,系统现在应该会向控制台报告互动情况,每当出现新的最慢互动时都会更新。例如,请尝试在搜索框中输入内容,然后删除输入内容。
5. 归因包含哪些内容?
我们先从大多数用户与该网页的首次互动入手,即 Cookie 意见征求对话框。
许多网页都会有脚本,需要在用户接受 Cookie 时同步触发 Cookie,这会导致点击互动变慢。这就是这里发生的情况。
点击是以接受(演示版)Cookie,然后查看现在已记录到 DevTools 控制台的 INP 数据。
标准版和归因版 Web Vitals build 中都提供了此顶级信息:
{
name: 'INP',
value: 344,
rating: 'needs-improvement',
entries: [...],
id: 'v4-1715732159298-8028729544485',
navigationType: 'reload',
attribution: {...},
}
从用户点击到下一次绘制所用的时间为 344 毫秒,INP 为“需要改进”。entries
数组包含与此互动相关的所有 PerformanceEntry
值,在本例中,只有一个点击事件。
不过,为了了解在此期间发生了什么,我们最感兴趣的是 attribution
属性。为了构建归因数据,web-vitals
会查找与点击事件重叠的长动画帧 (LoAF)。然后,LoAF 可以提供有关该帧期间所花时间的详细数据,从运行的脚本到 requestAnimationFrame
回调、样式和布局中花费的时间。
展开 attribution
属性可查看更多信息。数据更加丰富。
attribution: {
interactionTargetElement: Element,
interactionTarget: '#confirm',
interactionType: 'pointer',
inputDelay: 27,
processingDuration: 295.6,
presentationDelay: 21.4,
processedEventEntries: [...],
longAnimationFrameEntries: [...],
}
首先,是与哪些内容互动的信息:
interactionTargetElement
:对所互动元素的实时引用(如果该元素尚未从 DOM 中移除)。interactionTarget
:用于在网页中查找元素的选择器。
接下来,我们将概要介绍时间安排:
inputDelay
:从用户开始互动(例如,点击鼠标)到该互动对应的事件监听器开始运行之间的时间。在本例中,即使启用了 CPU 节流,输入延迟也只有大约 27 毫秒。processingDuration
:事件监听器运行完成所需的时间。通常,网页会为单个事件(例如pointerdown
、pointerup
和click
)设置多个监听器。如果它们都在同一动画帧中运行,则会合并到此时间。在本例中,处理时长为 295.6 毫秒,占 INP 时间的大部分。presentationDelay
:从事件监听器完成到浏览器完成绘制下一帧所用的时间。在本例中,为 21.4 毫秒。
这些 INP 阶段对于诊断需要优化的内容至关重要。“优化 INP”指南中提供了有关此主题的更多信息。
深入探究一下,processedEventEntries
包含 5 个事件,而顶级 INP entries
数组中只有 1 个事件。两者有何差异?
processedEventEntries: [
{
name: 'mouseover',
entryType: 'event',
startTime: 1801.6,
duration: 344,
processingStart: 1825.3,
processingEnd: 1825.3,
cancelable: true
},
{
name: 'mousedown',
entryType: 'event',
startTime: 1801.6,
duration: 344,
processingStart: 1825.3,
processingEnd: 1825.3,
cancelable: true
},
{name: 'mousedown', ...},
{name: 'mouseup', ...},
{name: 'click', ...},
],
顶级条目是 INP 事件,在本例中为点击。归因 processedEventEntries
是指在同一帧期间处理的所有事件。请注意,它包含 mouseover
和 mousedown
等其他事件,而不仅仅是点击事件。如果这些其他事件的响应速度也较慢,了解这些事件可能至关重要,因为它们都会导致响应速度缓慢。
最后是 longAnimationFrameEntries
数组。这可能是一个条目,但在某些情况下,互动可能会跨多个帧进行。下面是最简单的情况,只有一个长动画帧。
longAnimationFrameEntries
展开 LoAF 条目:
longAnimationFrameEntries: [{
name: 'long-animation-frame',
startTime: 1823,
duration: 319,
renderStart: 2139.5,
styleAndLayoutStart: 2139.7,
firstUIEventTimestamp: 1801.6,
blockingDuration: 268,
scripts: [{...}]
}],
这里包含许多有用的值,例如拆分样式设置所花费的时间。“Long Animation Frames API”一文详细介绍了这些属性。目前,我们主要关注 scripts
属性,其中包含的条目会详细说明负责长时间运行的帧的脚本:
scripts: [{
name: 'script',
invoker: 'BUTTON#confirm.onclick',
invokerType: 'event-listener',
startTime: 1828.6,
executionStart: 1828.6,
duration: 294,
sourceURL: 'http://localhost:8080/third-party/cmp.js',
sourceFunctionName: '',
sourceCharPosition: 1144
}]
在本例中,我们可以得知时间主要花在了对 BUTTON#confirm.onclick
调用的单个 event-listener
上。我们甚至可以看到脚本源代码网址和函数定义的字符位置!
要点总结
我们可以根据这些归因数据确定此支持请求的哪些方面?
- 互动是通过点击
button#confirm
元素触发的(来自attribution.interactionTarget
和脚本归因条目中的invoker
属性)。 - 主要用于执行事件监听器(
attribution.processingDuration
与总指标value
相比)。 - 慢速事件监听器代码从
third-party/cmp.js
中定义的点击监听器(从scripts.sourceURL
开始)开始。
这些数据足以让我们了解需要优化哪些方面!
6. 多个事件监听器
刷新页面,以清除 DevTools 控制台,并使 Cookie 意见征求互动不再是时长最长的互动。
开始在搜索框中输入内容。归因数据会显示哪些信息?您认为发生了什么情况?
归因数据
首先,概要介绍一下测试演示的示例:
{
name: 'INP',
value: 1072,
rating: 'poor',
attribution: {
interactionTargetElement: Element,
interactionTarget: '#search-terms',
interactionType: 'keyboard',
inputDelay: 3.3,
processingDuration: 1060.6,
presentationDelay: 8.1,
processedEventEntries: [...],
longAnimationFrameEntries: [...],
}
}
这是与 input#search-terms
元素的键盘互动产生的 INP 值较差(启用了 CPU 节流)。大部分时间(1072 毫秒总 INP 中的 1061 毫秒)都花在了处理时长上。
不过,scripts
条目更有趣。
布局抖动
scripts
数组的第一个条目提供了一些有价值的上下文:
scripts: [{
name: 'script',
invoker: 'BUTTON#confirm.onclick',
invokerType: 'event-listener',
startTime: 4875.6,
executionStart: 4875.6,
duration: 497,
forcedStyleAndLayoutDuration: 388,
sourceURL: 'http://localhost:8080/js/index.js',
sourceFunctionName: 'handleSearch',
sourceCharPosition: 940
},
...]
大部分处理时长都发生在此脚本执行期间,该脚本是一个 input
监听器(调用方为 INPUT#search-terms.oninput
)。函数名称已指定(handleSearch
),index.js
源文件中的字符位置也是如此。
不过,有一个新属性:forcedStyleAndLayoutDuration
。这是在浏览器被迫重新布局网页的此脚本调用中所花费的时间。换句话说,在执行此事件监听器时,78% 的时间(497 毫秒中的 388 毫秒)实际上都花在了布局抖动上。
这应该是优先解决的问题。
重复监听器
下面两个脚本条目各自没有什么特别之处:
scripts: [...,
{
name: 'script',
invoker: '#document.onkeyup',
invokerType: 'event-listener',
startTime: 5375.3,
executionStart: 5375.3,
duration: 124,
sourceURL: 'http://localhost:8080/js/index.js',
sourceFunctionName: '',
sourceCharPosition: 1526,
},
{
name: 'script',
invoker: '#document.onkeyup',
invokerType: 'event-listener',
startTime: 5673.9,
executionStart: 5673.9,
duration: 95,
sourceURL: 'http://localhost:8080/js/index.js',
sourceFunctionName: '',
sourceCharPosition: 1526
}]
这两个条目都是 keyup
监听器,会接连执行。监听器是匿名函数(因此 sourceFunctionName
属性中不会报告任何内容),但我们仍然有源文件和字符位置,因此可以找到代码所在的位置。
奇怪的是,这两个错误都来自同一源文件和字符位置。
浏览器最终会在单个动画帧中处理多次按键操作,导致此事件监听器在绘制任何内容之前运行两次!
这种影响还会叠加,事件监听器完成所需的时间越长,可能收到的其他输入事件就越多,从而延长缓慢互动的时间。
由于这是搜索/自动补全互动,因此对输入进行去抖动是一项不错的策略,这样每帧最多只会处理一次按键操作。
7. 输入延迟
输入延迟(从用户互动到事件监听器可以开始处理互动的时间)的典型原因是主线程繁忙。这可能有多种原因:
- 网页正在加载,主线程正忙于执行初始工作,包括设置 DOM、排版和设置网页样式,以及评估和运行脚本。
- 网页通常处于繁忙状态,例如正在运行计算、基于脚本的动画或广告。
- 上一个示例中显示了,之前的互动花费了太长时间来处理,导致后续互动延迟。
此演示页面有一个秘密功能:如果您点击页面顶部的蜗牛徽标,它将开始呈现动画效果并执行一些繁重的主线程 JavaScript 工作。
- 点击蜗牛徽标即可启动动画。
- 当蜗牛位于弹跳底部时,JavaScript 任务会触发。尝试在跳出率接近最低时与网页互动,看看您能触发多高的 INP。
例如,即使您未触发任何其他事件监听器(例如,在蜗牛弹跳时点击并将焦点移至搜索框),主线程工作也会导致页面在明显的时间段内无响应。
在许多网页上,主线程繁重工作不会表现得如此良好,但这是一个很好的演示,可以了解如何在 INP 归因数据中识别此类工作。
下面是一个仅在蜗牛式跳出期间将焦点放在搜索框上的归因示例:
{
name: 'INP',
value: 728,
rating: 'poor',
attribution: {
interactionTargetElement: Element,
interactionTarget: '#search-terms',
interactionType: 'pointer',
inputDelay: 702.3,
processingDuration: 4.9,
presentationDelay: 20.8,
longAnimationFrameEntries: [{
name: 'long-animation-frame',
startTime: 2064.8,
duration: 790,
renderStart: 2065,
styleAndLayoutStart: 2854.2,
firstUIEventTimestamp: 0,
blockingDuration: 740,
scripts: [{...}]
}]
}
}
正如预期,事件监听器的执行速度很快,处理时长为 4.9 毫秒,并且大部分互动问题都花在了输入延迟上,在总计 728 毫秒中,输入延迟就占了 702.3 毫秒。
这种情况可能很难调试。虽然我们知道用户与哪些内容互动以及互动方式,但我们也知道该部分互动很快就完成了,没有任何问题。而是页面上的其他内容延迟了互动开始处理的时间,但我们如何知道从哪里开始查找?
LoAF 脚本条目可派上用场:
scripts: [{
name: 'script',
invoker: 'SPAN.onanimationiteration',
invokerType: 'event-listener',
startTime: 2065,
executionStart: 2065,
duration: 788,
sourceURL: 'http://localhost:8080/js/index.js',
sourceFunctionName: 'cryptodaphneCoinHandler',
sourceCharPosition: 1831
}]
尽管此函数与互动毫无关系,但它确实会减慢动画帧速率,因此会包含在与互动事件联接的 LoAF 数据中。
从中我们可以了解延迟互动处理的函数是如何触发的(由 animationiteration
监听器触发),具体是哪个函数负责处理,以及它在源文件中的位置。
8. 呈现延迟:更新无法绘制
呈现延迟是指从事件监听器运行完毕到浏览器能够在屏幕上绘制新帧(向用户显示可见反馈)所用的时间。
刷新页面以再次重置 INP 值,然后打开三线状菜单。打开时确实有点卡顿。
这是什么样的?
{
name: 'INP',
value: 376,
rating: 'needs-improvement',
delta: 352,
attribution: {
interactionTarget: '#sidenav-button>svg',
interactionType: 'pointer',
inputDelay: 12.8,
processingDuration: 14.7,
presentationDelay: 348.5,
longAnimationFrameEntries: [{
name: 'long-animation-frame',
startTime: 651,
duration: 365,
renderStart: 673.2,
styleAndLayoutStart: 1004.3,
firstUIEventTimestamp: 138.6,
blockingDuration: 315,
scripts: [{...}]
}]
}
}
这次,导致互动速度缓慢的大部分原因是呈现延迟。这意味着,主线程的任何阻塞操作都会在事件监听器完成后发生。
scripts: [{
entryType: 'script',
invoker: 'FrameRequestCallback',
invokerType: 'user-callback',
startTime: 673.8,
executionStart: 673.8,
duration: 330,
sourceURL: 'http://localhost:8080/js/side-nav.js',
sourceFunctionName: '',
sourceCharPosition: 1193,
}]
查看 scripts
数组中的单个条目,我们会发现时间是在 FrameRequestCallback
的 user-callback
中花费的。这次,呈现延迟是由 requestAnimationFrame
回调引起的。
9. 总结
汇总现场数据
值得注意的是,如果您查看单次网页加载的单个 INP 归因条目,则会发现所有这些都更容易理解。如何汇总这些数据,以便根据现场数据调试 INP?事实上,有用的详细信息越多,这项工作就越难。
例如,了解哪个页面元素是导致互动缓慢的常见原因非常有用。不过,如果您的网页具有的已编译 CSS 类名称因 build 而异,则同一元素的 web-vitals
选择器在不同 build 中可能不同。
相反,您必须考虑自己的特定应用,以确定哪些数据最有用以及如何汇总数据。例如,在回传归因数据之前,您可以根据目标所在的组件或目标所实现的 ARIA 角色,将 web-vitals
选择器替换为您自己的标识符。
同样,scripts
条目可能在其 sourceURL
路径中包含基于文件的哈希,这会使其难以组合,但您可以在将数据回传信标之前根据已知的构建流程剥离哈希。
遗憾的是,对于如此复杂的数据,没有简单的解决方法,但即使使用其中的一部分数据,也比在调试过程中完全没有归因数据更有价值。
随时随地提供提供归属信息!
基于 LoAF 的 INP 归因是一款强大的调试辅助工具。它提供有关 INP 期间具体发生了什么的细化数据。在许多情况下,它可以指明您应从脚本中的哪个确切位置开始进行优化。
现在,您可以在任何网站上使用 INP 归因数据了!
即使您无权修改网页,也可以在开发者工具控制台中运行以下代码段,重现此 Codelab 中的流程,看看能找到什么:
const script = document.createElement('script');
script.src = 'https://unpkg.com/web-vitals@4/dist/web-vitals.attribution.iife.js';
script.onload = function () {
webVitals.onINP(console.log, {reportAllChanges: true});
};
document.head.appendChild(script);