优化 Interaction to Next Paint

了解如何优化网站的 Interaction to Next Paint。

Interaction to Next Paint (INP) 是一项稳定的 Core Web Vitals 指标,通过观察用户在用户访问网页的整个生命周期内发生的所有符合条件的互动的延迟时间,来评估网页对用户互动的总体响应情况。最终 INP 值是观察到的最长交互(有时忽略离群值)。

为了提供良好的用户体验,网站应尽力将 Interaction to Next Paint 控制在 200 毫秒或更短的范围内。为确保大多数用户都能达到此目标值,建议将网页加载的第 75 个百分位作为阈值(按移动设备和桌面设备细分)。

良好的 INP 值不超过 200 毫秒,不佳的值大于 500 毫秒,介于两者之间的所有值都需要改进。

根据网站的不同,互动元素可能很少,甚至没有,例如网页主要是文字和图片,互动元素很少,甚至完全没有。或者,在文本编辑器或游戏等网站上,可能会发生数百次甚至数千次互动。无论是哪种情况,如果 INP 较高,用户体验就会面临风险。

改进 INP 需要投入时间和精力,但奖励能为他们提供更好的用户体验。本指南将探讨改进 INP 的途径。

找出导致 INP 不佳的原因

您需要先获得数据,以便了解您网站的 INP 不佳或需要改进,然后才能解决互动缓慢问题。获得这些信息后,您就可以进入实验室,开始诊断缓慢互动问题,然后设法找出解决方案。

找出现场中的慢互动

理想情况下,优化 INP 的历程将从实测数据入手。通过真实用户监控 (RUM) 提供程序提供的现场数据,您不仅可以获得网页的 INP 值,还可以提供情境数据,这些数据会突出显示导致 INP 值本身的具体互动、互动发生在网页加载期间还是之后、互动类型(点击、按键或点按)以及其他有价值的信息。

如果您不依赖 RUM 提供商来获取现场数据,请参阅 INP 字段数据指南,建议您通过 PageSpeed Insights 使用 Chrome 用户体验报告 (CrUX),以帮助填补数据缺口。CrUX 是 Core Web Vitals 计划的官方数据集,它为包括 INP 在内的数百万个网站提供指标的概要摘要。但是,CrUX 通常不会提供从 RUM 提供商处获得的情境数据来帮助您分析问题。因此,我们仍建议网站尽可能使用 RUM 提供商,或者实现自己的 RUM 解决方案来补充 CrUX 中提供的内容。

诊断实验室中的缓慢交互问题

理想情况下,当您获得表明您的互动速度缓慢的实测数据后,最好开始在实验室中进行测试。在没有实测数据的情况下,您可以使用一些策略来识别实验室中的慢互动。这些策略包括遵循常见的用户流并在整个过程中测试互动,以及在加载期间(主线程通常最繁忙时)与页面交互,以便在用户体验的关键部分显示缓慢的互动。

优化互动

发现缓慢互动并可以在实验中手动重现该互动后,下一步就是对其进行优化。互动可分为三个阶段:

  1. 输入延迟,在用户发起与网页的互动时开始,在互动的事件回调开始运行时结束。
  2. 处理时长,即事件回调完成运行所需的时间。
  3. 呈现延迟,即浏览器呈现包含互动视觉结果的下一帧所用的时间。

这三个阶段的总和就是总互动延迟时间。互动的每个阶段都会带来一定程度的总互动延迟时间,因此请务必了解如何优化互动的各个部分,以便尽可能缩短互动时间。

识别并缩短输入延迟

当用户与页面交互时,该交互的第一部分是“输入延迟”。根据页面上的其他活动,输入延迟可能会相当长。这可能是由于主线程上发生的活动(可能是由于脚本加载、解析和编译)、提取处理、计时器函数,甚至是由于快速连续发生的、彼此重叠的其他交互发生的。

无论互动的输入延迟来源是什么,您都需要将输入延迟减少到最低程度,以便互动能够尽快开始运行事件回调。

启动期间脚本评估与长时间运行的任务之间的关系

启动期间是页面生命周期中互动的一个关键方面。网页加载时,网页最初会呈现;但请注意,网页已呈现并不意味着网页已加载完毕。根据页面完全正常运行所需的资源数量,用户可能会尝试在页面加载期间与其互动。

在网页加载时可以延长互动的输入延迟的一个方法是脚本评估。从网络获取 JavaScript 文件后,浏览器还需要完成一些工作才能运行该 JavaScript;该工作包括解析脚本以确保其语法有效,将其编译为字节码,然后最终执行它。

根据脚本的大小,这项工作可能会在主线程上引入耗时较长的任务,从而导致浏览器延迟响应其他用户交互。为了让您的网页在网页加载期间能够及时响应用户输入,请务必了解您可以采取哪些措施来降低网页加载过程中耗时较长的任务的可能性,从而保持网页加载速度。

优化事件回调

输入延迟只是 INP 衡量的第一部分。您还需要确保为响应用户互动而运行的事件回调可以尽快完成。

经常让给主线程

在优化事件回调时,最好的做法是尽可能少做一些工作。不过,您的互动逻辑可能比较复杂,您可能只能稍微减少它们所做的工作。

如果您发现网站就是这种情况,那么接下来可以尝试将事件回调中的工作拆分成单独的任务。这样可以防止集体工作变成阻塞主线程的冗长任务,从而让原本需要等待主线程的其他交互更快地运行。

setTimeout 是分解任务的一种方式,因为传递给它的回调会在新任务中运行。您可以单独使用 setTimeout,也可以将其用法抽象为单独的函数,以符合更符合工效学要求的产出方式。

不加选择地让出比完全不让出要好,不过,还有一种更精细的让出方法,这涉及到在更新界面的事件回调之后立即让出,以便渲染逻辑能够更快运行。

让渲染工作更快完成

更高级的生成方法包括在事件回调中构建代码,以便将运行的内容限制为仅对下一帧应用视觉更新所需的逻辑。其他操作可以推迟到后续任务中。这不仅使回调保持轻巧、灵活,而且还允许视觉更新阻止事件回调代码,从而缩短了交互的渲染时间。

例如,假设有一款富文本编辑器,它会在您输入时设置文本格式,还会根据您所输入的内容更新界面的其他方面(例如字数、突出显示拼写错误以及其他重要的视觉反馈)。此外,该应用可能还需要保存您已撰写的内容,以便在离开或返回时不会丢失任何工作内容。

在此示例中,为了响应用户输入的字符,需要执行以下四项操作。不过,只需在显示下一帧之前完成第一项即可。

  1. 使用用户输入的内容更新文本框,并应用任何所需的格式。
  2. 更新界面中显示当前字数的部分。
  3. 运行逻辑以检查是否存在拼写错误。
  4. 保存最新的更改(在本地或远程数据库)。

执行此操作的代码可能如下所示:

textBox.addEventListener('input', (inputEvent) => {
  // Update the UI immediately, so the changes the user made
  // are visible as soon as the next frame is presented.
  updateTextBox(inputEvent);

  // Use `setTimeout` to defer all other work until at least the next
  // frame by queuing a task in a `requestAnimationFrame()` callback.
  requestAnimationFrame(() => {
    setTimeout(() => {
      const text = textBox.textContent;
      updateWordCount(text);
      checkSpelling(text);
      saveChanges(text);
    }, 0);
  });
});

下图显示了将任何非关键更新延迟到下一帧之后如何缩短处理时长,从而缩短总体交互延迟时间。

对两种场景下的键盘互动和后续任务的描绘。在上图中,渲染关键任务和所有后续后台任务会同步运行,直到出现呈现帧的机会。在下图中,渲染关键型工作首先运行,然后让到主线程,以更快地呈现新帧。此后运行后台任务。
点击上图可查看高分辨率版本。

虽然在上一个代码示例中的 requestAnimationFrame() 调用中使用 setTimeout() 有点隐秘,但这是一种适用于所有浏览器的有效方法,可以确保非关键代码不会阻塞下一帧。

避免布局抖动

布局抖动(有时称为强制同步布局)是布局同步发生的渲染性能问题。当您在 JavaScript 中更新样式,然后在同一任务中读取这些样式时,就会发生这种情况,并且 JavaScript 中的许多属性可能会导致布局抖动

Chrome 开发者工具的性能面板中显示的布局抖动可视化效果。
布局抖动示例,如 Chrome 开发者工具的性能面板中所示。涉及布局抖动的渲染任务会在调用堆栈部分的右上角显示一个红色三角形(通常标记为 Recalculate StyleLayout)。

布局抖动是性能瓶颈,因为通过更新样式,然后立即在 JavaScript 中请求这些样式的值,浏览器被迫执行同步布局工作,否则本可能要在事件回调结束后等待异步执行。

尽可能缩短展示延迟时间

互动的呈现延迟标记了从互动的事件回调运行完毕开始,到浏览器能够绘制下一帧显示所产生视觉变化的时间点。

尽量减小 DOM 大小

当页面的 DOM 较小时,渲染工作通常很快就会完成。不过,如果 DOM 非常大,渲染工作往往会随着 DOM 规模的扩大而扩展。渲染工作和 DOM 大小之间的关系不是线性的,但与小型 DOM 相比,大型 DOM 确实需要更多的工作来进行渲染。大型 DOM 在以下两种情况下会出现问题:

  1. 在初始页面渲染期间,大型 DOM 需要执行大量工作来渲染页面的初始状态。
  2. 在响应用户互动时,大型 DOM 可能会导致渲染更新的开销非常大,因而浏览器显示下一帧所需的时间也会增加。

请注意,在某些情况下,系统无法大幅减少大型 DOM。尽管您可以采用多种方法来减小 DOM 大小,如将 DOM 扁平化在用户互动期间向 DOM 添加内容以保持初始 DOM 大小,但这些方法可能只能收效甚微。

使用 content-visibility 延迟渲染屏幕外的元素

如需限制网页加载期间的渲染工作量和响应用户互动的渲染工作量,一种方法是依赖 CSS content-visibility 属性,这实际上相当于在元素接近视口时延迟渲染。虽然 content-visibility 需要经过一些练习才能有效使用,但如果能缩短呈现时间,从而改善网页的 INP,那还是值得调查一下。

注意在使用 JavaScript 呈现 HTML 时会降低性能

只要有 HTML,需要进行 HTML 解析,浏览器完成 HTML 解析到 DOM 后,必须对其应用样式、执行布局计算,然后呈现该布局。这是不可避免的成本,但是呈现 HTML 的方式至关重要。

当服务器发送 HTML 时,相应 HTML 就会以流的形式到达浏览器。流式处理是指来自服务器的 HTML 响应以分块的形式送达。浏览器通过在数据流块到达时逐步解析并逐块呈现数据流块来优化其处理数据流的方式。这是一种性能优化,因为浏览器在页面加载期间隐式地定期生成,而您可以免费获得。

虽然首次访问任何网站总是会涉及到一定数量的 HTML,但通常的做法是先使用少量的 HTML 代码,然后再使用 JavaScript 填充内容区域。用户互动之后,相应内容区域的后续更新也会随之更新。这通常称为单页应用 (SPA) 模型。这种模式的一个缺点是,通过在客户端上使用 JavaScript 呈现 HTML,您不仅需要支付创建相应 HTML 的 JavaScript 处理费用,而且浏览器在解析和呈现完该 HTML 之前不会产生任何收益。

但请务必注意,即使是 SPA 的网站,也可能因为互动而通过 JavaScript 呈现一定量的 HTML。这通常没什么问题,只要您不是在客户端上呈现大量 HTML 就会延迟下一帧的呈现。不过,您有必要了解这种在浏览器中呈现 HTML 的方法对性能的影响,以及如果您通过 JavaScript 呈现大量 HTML,该方法会如何影响您的网站对用户输入的响应情况。

总结

改进网站的 INP 是一个迭代过程。只要解决在现场操作中缓慢的互动问题,就很有可能会 - 特别是如果您的网站提供大量互动操作 - 您将开始发现其他速度缓慢的互动,因此也需要对其进行优化。

提高 INP 的关键是持久性。过一段时间后,您可以在用户对您提供的体验感到满意的位置,使网页响应速度。此外,在为用户开发新功能时,您可能需要按照相同的流程来优化特定于用户的互动。这需要时间和精力,但它们是值得的。

David Pisnoy 创作的 Un 应用程序 中的主打图片,并根据 Un 载入许可进行了修改。