优化 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,然后使用 JavaScript 填充内容区域。该内容区域的后续更新也是用户交互的结果。这通常称为单页应用 (SPA) 模型。这种模式的一个缺点是,如果使用 JavaScript 在客户端上呈现 HTML,不仅会带来创建该 HTML 的 JavaScript 处理成本,而且浏览器在解析并呈现该 HTML 之前不会让出它。

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

总结

改进网站的 INP 是一个迭代过程。在解决现场互动速度缓慢的问题后,尤其是在网站提供大量互动时,您很有可能会开始发现其他缓慢的互动,并且需要对其进行优化。

提高 INP 的关键是持久性。用不了多久,您就可以让网页在用户对您提供的体验感到满意时迅速响应。在为用户开发新功能时,您也可能需要遵循相同的流程来优化专门针对他们的互动。这需要时间和精力,但是要付出很多时间和精力。

主打图片来自 Unsplash 用户,由 David Pisnoy 制作,并且已根据 Unsplash 许可进行了修改。