优化 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 开发者工具的“Performance”(性能)面板中所示。
布局抖动示例,如 Chrome DevTools 的“性能”面板中所示。涉及布局抖动的渲染任务会在调用堆栈相应部分的右上角显示一个红色三角形,通常标记为重新计算样式布局

布局抖动是性能瓶颈,因为通过更新样式,然后立即在 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 的关键在于坚持不懈。随着时间的推移,您可以让网页的响应速度达到理想水平,让用户对您提供的体验感到满意。在为用户开发新功能时,您很可能也需要遵循相同的过程来优化面向用户的互动。这需要花费时间和精力,但这些时间和精力是值得的。

主打图片来自 Unsplash,作者为 David Pisnoy,并已根据 Unsplash 许可进行了修改。