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 中,我们将同时使用性能面板和控制台。您可以随时通过开发者工具顶部的标签页在这两者之间切换。
- INP 问题最常发生在移动设备上,因此请切换到移动显示模拟。
- 如果您在桌面设备或笔记本电脑上进行测试,性能可能会比在真实的移动设备上好得多。如需更真实地了解性能,请点击性能面板右上角的齿轮,然后选择 CPU 减速 4 倍。

4. 安装 web-vitals
web-vitals 是一个 JavaScript 库,用于衡量用户体验到的 Web Vitals 指标。您可以使用该库捕获这些值,然后将它们信标到分析端点以供日后分析,以便我们了解何时何地发生缓慢的互动。
您可以通过几种不同的方式将库添加到网页中。您在自己的网站上安装该库的方式取决于您管理依赖项、构建流程和其他因素的方式。请务必查看库的文档,了解所有选项。
本 Codelab 将从 npm 安装并直接加载脚本,以避免深入了解特定的构建流程。
您可以使用两个版本的 web-vitals:
- 如果您想在网页加载时跟踪 Core 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,然后查看现在已记录到开发者工具控制台中的 INP 数据。

标准版和归因版 Web 指标 build 中均提供此顶级信息:
{
name: 'INP',
value: 344,
rating: 'needs-improvement',
entries: [...],
id: 'v4-1715732159298-8028729544485',
navigationType: 'reload',
attribution: {...},
}
从用户点击到下一次绘制的时间长度为 344 毫秒,属于 “需要改进”的 INP。entries 数组包含与相应互动关联的所有 PerformanceEntry 值,在本例中,只有一个点击事件。
不过,为了了解这段时间发生了什么,我们最感兴趣的是 attribution 属性。为了构建归因数据,web-vitals 会查找与点击事件重叠的 Long Animations Frame (LoAF)。然后,LoAF 可以提供有关该帧期间时间使用情况的详细数据,包括运行的脚本、在 requestAnimationFrame 回调、样式和布局中花费的时间。
展开 attribution 属性即可查看更多信息。数据更加丰富。
attribution: {
interactionTargetElement: Element,
interactionTarget: '#confirm',
interactionType: 'pointer',
inputDelay: 27,
processingDuration: 295.6,
presentationDelay: 21.4,
processedEventEntries: [...],
longAnimationFrameEntries: [...],
}
首先,是有关互动对象的信息:
interactionTargetElement:与用户互动过的元素的实时引用(如果该元素尚未从 DOM 中移除)。interactionTarget:用于查找网页中元素的 selector。
接下来,我们以概括的方式细分时间安排:
inputDelay:用户开始互动(例如点击鼠标)到该互动的事件监听器开始运行之间的时间。在这种情况下,即使开启了 CPU 节流,输入延迟也仅为 27 毫秒左右。processingDuration:事件监听器运行到完成所需的时间。网页通常会为单个事件(例如pointerdown、pointerup和click)设置多个监听器。如果这些监听器都在同一动画帧中运行,则会合并到此时间。在这种情况下,处理时长为 295.6 毫秒,占 INP 时间的大部分。presentationDelay:从事件监听器完成到浏览器完成绘制下一帧的时间。在本例中,为 21.4 毫秒。
这些 INP 阶段可以作为诊断需要优化的内容的关键信号。如需详细了解此主题,请参阅优化 INP 指南。
深入了解一下,processedEventEntries 包含 5 个事件,而顶级 INP entries 数组中只有一个事件。两者有何差异?
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. 多个事件监听器
刷新网页,使开发者工具控制台保持清空状态,并且 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?有用的细节太多反而会使问题变得更加复杂。
例如,了解哪个页面元素是导致互动缓慢的常见原因非常有用。不过,如果您的网页具有从 build 到 build 都会发生变化的已编译 CSS 类名称,那么同一元素的 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);