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 使用腹足(一种热门的蜗牛解剖参考网站)来探索 INP 的潜在问题。
尝试与网页互动,了解哪些互动速度较慢。
3. 了解 Chrome 开发者工具中的现状
从更多工具 > 打开开发者工具开发者工具菜单,右键点击页面,然后选择检查,或使用键盘快捷键。
在此 Codelab 中,我们将使用 Performance 面板和 Console。您可以随时在开发者工具顶部的标签页中切换这些模式。
- INP 问题最常发生在移动设备上,因此请切换到移动显示模拟。
- 如果您在台式机或笔记本电脑上进行测试,效果可能会明显优于真实移动设备。如需更真实地了解性能,请点击性能面板右上角的齿轮图标,然后选择 CPU 4x 减速。
4. 正在安装 web-vitals
web-vitals
是一个 JavaScript 库,用于衡量用户体验的网页指标指标。您可以使用该库捕获这些值,然后通过信标将其转至某个分析端点,以便日后进行分析,以便我们能够计算出缓慢互动的发生时间和位置。
您可以通过几种不同的方法将库添加到页面中。如何在您自己的网站上安装库取决于您管理依赖项的方式、构建流程和其他因素。请务必查看该库的文档,了解所有可用选项。
此 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,导致点击成为缓慢的互动。这就是发生的情况。
点击 Yes(是)以接受(演示)Cookie,并查看现在已记录到开发者工具控制台中的 INP 数据。
标准和归因网站指标 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
包含五个事件,而不是顶级 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% 的时间(388 毫秒/497 毫秒)实际上都花在了布局抖动上。
此问题应该是当务之急。
重复的监听器
具体而言,下面两个脚本条目没有什么特别值得注意的方面:
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 毫秒,而绝大部分不良互动都花在输入延迟上,用了 702.3 毫秒(共 728 毫秒)。
这种情况可能难以调试。虽然我们了解用户与之互动的内容和方式,但也知道这部分互动很快完成并且不成问题。而是网页上的其他内容导致系统无法开始处理互动,但我们如何知道从何处开始查找呢?
为帮助您节省时间:
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);