了解 Interaction to Next Paint (INP)

1. 简介

这是一个互动演示 Codelab,用于了解 Interaction to Next Paint (INP)

一张图表,描绘了主线程上的互动。用户在阻塞任务运行时进行输入。输入会延迟到这些任务完成,之后 pointerup、mouseup 和点击事件监听器会运行,然后开始渲染和绘制工作,直到呈现下一帧

前提条件

  • 了解 HTML 和 JavaScript 开发。
  • 建议:阅读 INP 文档

学习内容

  • 用户互动与您对这些互动的处理如何共同影响网页的响应速度。
  • 如何减少和消除延迟,以实现顺畅的用户体验。

所需条件

  • 一台能够从 GitHub 克隆代码并运行 npm 命令的计算机。
  • 文本编辑器。
  • 较新版本的 Chrome,以便所有互动衡量指标都能正常发挥作用。

2. 进行设置

获取并运行代码

该代码位于 web-vitals-codelabs 代码库中。

  1. 在终端中克隆代码库:git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
  2. 进入克隆的目录:cd web-vitals-codelabs/understanding-inp
  3. 安装依赖项:npm ci
  4. 启动 Web 服务器:npm run start
  5. 在浏览器中访问 http://localhost:5173/understanding-inp/

应用概览

页面顶部有一个得分计数器和一个递增按钮。这是一个经典的反应和响应速度演示示例!

此 Codelab 的演示版应用的屏幕截图

按钮下方有四项指标:

  • INP:当前 INP 得分,通常是最糟糕的互动。
  • Interaction:最近一次互动的得分。
  • FPS:网页的主线程每秒帧数。
  • Timer:一个正在运行的计时器动画,有助于更直观地反映卡顿。

FPS 和 Timer 条目对衡量互动完全不必要。添加它们只是为了更便于直观呈现响应速度。

试试看

尝试与 Increment 按钮互动,你应该会看到得分增加。INP互动值是否会随着每次增量而变化?

INP 衡量的是从用户发起互动到网页向用户实际显示渲染的更新内容之间的用时。

3. 使用 Chrome 开发者工具衡量互动

通过以下方式打开开发者工具依次选择更多工具 > 开发者工具右键点击页面并选择检查;或使用键盘快捷键

切换到性能面板,您将使用该面板来衡量互动。

开发者工具“性能”面板与应用并排显示的屏幕截图

接下来,在“性能”面板中捕获一次互动。

  1. 按住录制按钮开始录制。
  2. 与网页互动(按 Increment 按钮)。
  3. 停止录制。

在生成的时间轴中,您会看到一个互动轨道。点击左侧的三角形即可展开。

使用开发者工具的“性能”面板记录互动的动画演示

系统会显示两次互动。滚动或按住 W 键可放大第二个。

开发者工具“性能”面板的屏幕截图,光标悬停在面板中的互动上,并显示列出互动简短时间的提示

将鼠标悬停在互动上,您会看到该互动速度很快,在处理时长中没有花费时间,在输入延迟呈现延迟中花费的时间最少,具体时长取决于机器的速度。

4. 长时间运行的事件监听器

打开 index.js 文件,然后取消注释事件监听器内的 blockFor 函数。

查看完整代码:click_block.html

button.addEventListener('click', () => {
  blockFor(1000);
  score.incrementAndUpdateUI();
});

保存文件。服务器会看到相应更改,并为您刷新页面。

请尝试再次与网页互动。互动速度会明显变慢。

性能轨迹

在“性能”面板中再次录制,看看效果如何。

“性能”面板中的 1 秒互动

曾经只需短时间完成的互动现在需要整整一秒。

当您将鼠标悬停在互动上时,会发现时间几乎全部花费在“处理时长”上,这是执行事件监听器回调所花费的时间。由于阻塞 blockFor 调用完全在事件监听器内,因此时间都花在了这里。

5. 实验:处理时长

尝试重新安排事件监听器工作,看看对 INP 有何影响。

先更新界面

如果您交换 JS 调用的顺序,先更新界面,然后再屏蔽,会发生什么情况?

查看完整代码:ui_first.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  blockFor(1000);
});

您是否注意到界面更早显示了?顺序会影响 INP 分数吗?

尝试进行轨迹分析并检查互动情况,看看是否存在任何差异。

分离监听器

如果将工作移至单独的事件监听器会怎样?在一个事件监听器中更新界面,并从单独的监听器中阻止页面。

查看完整代码:two_click.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('click', () => {
  blockFor(1000);
});

现在在“性能”面板中是什么样的?

不同类型的活动

大多数互动都会触发多种类型的事件,从指针或按键事件到悬停、聚焦/模糊事件,再到 beforechange 和 beforeinput 等合成事件。

许多实际网页都有针对多种不同事件的监听器。

如果您更改事件监听器的事件类型,会发生什么情况?例如,将某个 click 事件监听器替换为 pointerupmouseup

查看完整代码:diff_handlers.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('pointerup', () => {
  blockFor(1000);
});

无界面更新

如果您从事件监听器中移除更新界面的调用,会发生什么情况?

查看完整代码:no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});

6. 处理时长实验结果

性能轨迹:先更新界面

查看完整代码:ui_first.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  blockFor(1000);
});

查看点击按钮的“性能”面板记录,您会发现结果并未发生变化。虽然在阻塞代码之前触发了界面更新,但浏览器实际上直到事件监听器完成之后才更新屏幕上绘制的内容,这意味着互动仍然需要花费一秒多的时间才能完成。

“性能”面板中时长为 1 秒的静态互动

性能轨迹:分离的监听器

查看完整代码:two_click.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('click', () => {
  blockFor(1000);
});

同样,在功能上没有任何区别。互动仍需要整整一秒。

如果您放大点击互动,就会发现 click 事件确实导致了两个不同的函数被调用。

正如预期的那样,第一个(更新界面)运行速度非常快,而第二个则需要整整一秒。不过,这些因素的综合影响会导致最终用户体验到相同的缓慢互动。

此示例中时长为 1 秒的互动放大视图,显示第一个函数调用在不到 1 毫秒的时间内完成

性能轨迹:不同的事件类型

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('pointerup', () => {
  blockFor(1000);
});

这些结果非常相似。互动时间仍为一整秒;唯一的区别是,较短的仅限界面更新的 click 监听器现在在阻塞的 pointerup 监听器之后运行。

此示例中时长为 1 秒的互动放大视图,显示了在 pointerup 监听器之后,点击事件监听器在不到 1 毫秒的时间内完成

性能轨迹:界面未更新

查看完整代码:no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});
  • 得分没更新,但网页仍会更新!
  • 动画、CSS 效果、默认网页组件操作(表单输入)、文本输入、文本突出显示等功能仍在不断更新。

在这种情况下,按钮在点击时会进入活动状态,然后返回,这需要浏览器进行绘制,这意味着仍然存在 INP。

由于事件监听器阻塞了主线程一秒钟,导致网页无法绘制,因此互动仍需要整整一秒钟。

录制性能面板时,显示的互动与之前的互动几乎完全相同。

“性能”面板中时长为 1 秒的静态互动

要点总结

任何事件监听器中运行的任何代码都会延迟互动。

  • 包括在不同脚本中注册的监听器,以及在监听器中运行的框架或库代码,例如触发组件渲染的状态更新。
  • 不仅限于您自己的代码,还包括所有第三方脚本。

这是一个常见问题!

最后:即使您的代码不会触发绘制,也不意味着绘制不会等待缓慢的事件监听器完成。

7. 实验:输入延迟

那么,事件监听器之外的长时间运行的代码呢?例如:

  • 如果您有一个在加载期间随机阻止网页的延迟加载 <script>
  • 定期阻止网页的 API 调用(例如 setInterval)?

尝试从事件监听器中移除 blockFor,并将其添加到 setInterval()

查看完整代码:input_delay.html

setInterval(() => {
  blockFor(1000);
}, 3000);


button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

说明

8. 输入延迟实验结果

查看完整代码:input_delay.html

setInterval(() => {
  blockFor(1000);
}, 3000);


button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

如果用户在 setInterval 阻塞任务运行时点击按钮,即使互动本身没有阻塞工作,也会导致长时间运行的互动!

这些长时间运行的周期通常称为长任务。

将鼠标悬停在开发者工具中的互动上,您会发现互动时间现在主要归因于输入延迟,而不是处理时长。

开发者工具的“性能”面板,显示了一个 1 秒的阻塞任务、在该任务进行到一半时发生的互动,以及一个 642 毫秒的互动(主要归因于输入延迟)

请注意,它并不总是会影响互动!如果您在任务运行时不点击,可能会侥幸成功。如果这种“随机”喷嚏只是偶尔会导致问题,那么调试起来会非常棘手。

一种找出这些问题的方法是衡量长任务(或长动画帧)和总阻塞时间

9. 演示速度缓慢

到目前为止,我们已经通过输入延迟或事件监听器了解了 JavaScript 的性能,但还有哪些因素会影响下一次绘制的渲染?

使用昂贵的效果更新网页!

即使网页更新很快,浏览器可能仍需花费大量时间来渲染这些网页!

在主线程中:

  • 界面框架需要在状态更改后渲染更新
  • 发生 DOM 更改或切换许多复杂的 CSS 查询选择器时,可能会触发大量的样式、布局和绘制操作。

在主线程外:

  • 使用 CSS 增强 GPU 效果
  • 添加超大高分辨率图像
  • 使用 SVG/Canvas 绘制复杂场景

网页上渲染的不同元素的草图

RenderingNG

以下是一些常见的网络示例:

  • 一个在点击链接后重建整个 DOM,但不会暂停以提供初始视觉反馈的 SPA 网站。
  • 一种搜索页面,可提供具有动态用户界面的复杂搜索过滤条件,但会运行开销很大的监听器来实现此目的。
  • 用于触发整个网页的样式/布局的深色模式切换开关

10. 实验:展示延迟

requestAnimationFrame运行缓慢

我们来使用 requestAnimationFrame() API 模拟较长的演示文稿延迟。

blockFor 调用移到 requestAnimationFrame 回调中,以便在事件监听器返回运行:

查看完整代码:presentation_delay.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

说明

11. 展示延迟实验结果

查看完整代码:presentation_delay.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

互动时长仍为 1 秒,那么发生了什么?

requestAnimationFrame 请求在下一次绘制之前执行回调。由于 INP 衡量的是从互动到下一次绘制的时间,因此 requestAnimationFrame 中的 blockFor(1000) 会继续阻止下一次绘制整整一秒。

“性能”面板中时长为 1 秒的静态互动

不过,请注意以下两点:

  • 悬停时,您会看到所有互动时间现在都花费在“呈现延迟”上,因为主线程阻塞发生在事件监听器返回之后。
  • 主线程 activity 的根不再是点击事件,而是“Animation Frame Fired”。

12. 诊断互动

在此测试页面上,响应速度非常直观,有得分、计时器和计数器界面...但在测试普通页面时,响应速度就没那么直观了。

当互动持续时间较长时,我们并不总是清楚问题出在哪里。原因可能是:

  • 输入延迟?
  • 活动处理时长?
  • 演示延迟?

在任何所需的网页上,您都可以使用开发者工具来帮助衡量响应速度。如需养成此习惯,请尝试以下流程:

  1. 像平常一样浏览网页。
  2. 密切关注开发者工具“性能”面板的实时指标视图中的“互动”日志。
  3. 如果发现某项互动表现不佳,请尝试重复该互动:
  • 如果无法重复,请使用互动日志获取数据洞见。
  • 如果可以重复,请在“性能”面板中录制跟踪记录。

所有延迟

尝试在网页中添加一些包含所有这些问题的文本:

查看完整代码:all_the_things.html

setInterval(() => {
  blockFor(1000);
}, 3000);

button.addEventListener('click', () => {
  blockFor(1000);
  score.incrementAndUpdateUI();

  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

然后使用控制台和性能面板诊断问题!

13. 实验:异步工作

由于您可以在互动中启动非视觉效果(例如发出网络请求、启动计时器或仅更新全局状态),那么当这些效果最终更新网页时会发生什么情况?

只要互动后的下一次绘制可以进行渲染,即使浏览器确定实际上不需要对更新内容进行渲染,衡量互动的操作即会终止。

如需尝试此方法,请继续从点击监听器更新界面,但从超时中运行阻塞性工作。

查看完整代码:timeout_100.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

接下来会怎样?

14. 异步工作实验结果

查看完整代码:timeout_100.html

button.addEventListener(