优化 Interaction to Next Paint:把慢交互拆成三个阶段

优化 Interaction to Next Paint:把慢交互拆成三个阶段

作者:Jeremy Wagner、Philip Walton、Barry Pollard。原文:Optimize Interaction to Next Paint(web.dev)。发布于 2023 年 5 月 19 日,原页更新于 2025 年 9 月 2 日;来源复核于 2026 年 10 月 8 日。

本文依据原文完整翻译并做技术校订。编辑补充会单独标明。正文适用于已有交互性能数据、需要逐步定位卡顿的前端项目;单次本地测量不能代表所有真实用户。

Interaction to Next Paint(INP,交互到下一次绘制)是一项已经稳定的 Core Web Vitals 指标。它观察用户访问页面期间所有符合条件的交互延迟,用来评估页面对交互的整体响应速度。最终的 INP 值取自观察到的最慢交互,不过在某些情况下会忽略离群值。

为了提供良好体验,网站应争取把 INP 控制在 200 毫秒以内。要判断大多数用户是否达到目标,可以按移动设备和桌面设备分别统计页面访问数据,并考察第 75 百分位数。INP 不超过 200 毫秒属于良好;超过 200 毫秒、但不超过 500 毫秒需要改进;超过 500 毫秒则较差。

web.dev 原图所示的 INP 阈值:不超过 200 毫秒为良好,200 至 500 毫秒需要改进,超过 500 毫秒较差。
web.dev 原图“INP thresholds”,未修改;作者 Jeremy Wagner、Philip Walton、Barry Pollard,CC BY 4.0。原图来源。

有些页面主要由文字和图片构成,几乎没有交互;而文本编辑器、游戏等网站,一次访问中可能出现数百次甚至数千次交互。无论属于哪一种,只要 INP 偏高,用户体验就可能受到影响。改进 INP 需要时间和精力,回报则是更流畅的使用体验。

先找出 INP 不佳的原因

修复慢交互之前,先用数据确认网站的 INP 是否处于较差或需要改进的范围。确定问题后,再进入实验环境诊断,逐步找到解决办法。

从真实用户数据中发现慢交互

优化最好从线上数据开始。理想情况下,真实用户监测(RUM)服务不仅会提供页面的 INP 值,还会记录足够的上下文:究竟是哪次交互产生了这个值、交互发生在页面加载过程中还是加载完成后、交互类型是点击、按键还是轻触,以及其他有助于诊断的信息。

如果没有使用 RUM 服务,可以通过 PageSpeed Insights 查看 Chrome 用户体验报告(CrUX),补充一部分线上数据。CrUX 是 Google 在 Core Web Vitals 计划中使用的数据集,提供数百万个网站的指标概况,其中也包括 INP。但它通常不具备 RUM 服务那样细致的交互上下文,因此不足以独立解释某一次卡顿的成因。

条件允许时,仍应使用 RUM 服务,或者自行实现 RUM,补充 CrUX 的信息。相关方法见在真实用户数据中发现慢交互。

在实验环境中诊断慢交互

最好先拿到能指出慢交互的线上数据,再开展实验室测试。如果暂时没有这类数据,也可以沿着常见的用户操作路径逐步测试,或在页面仍在加载时尝试操作。加载阶段往往是主线程最繁忙的时候,因而特别值得检查。

具体方法见在实验环境中手动诊断慢交互。当你已经找到一项慢交互,而且能在实验环境中手动复现它,就可以开始优化。

把交互拆成三个阶段

一次交互可以拆成以下三个部分:

  1. 输入延迟(input delay):从用户发起交互,到该交互的事件回调开始执行。
  2. 处理时长(processing duration):执行相关事件回调所花的时间。
  3. 呈现延迟(presentation delay):事件回调完成后,浏览器把包含这次交互视觉结果的下一帧呈现出来之前的时间。
web.dev 原图:阻塞任务延迟用户输入,随后运行 pointerup、mouseup 和 click 回调,再执行渲染和绘制;输入延迟、处理时长、呈现延迟共同构成交互总时长。
web.dev 原图“An example interaction on the main thread”,未修改;作者 Jeremy Wagner、Philip Walton、Barry Pollard,CC BY 4.0。原图来源。
INP 三阶段时间线:用户输入后先等待主线程,再执行 pointerup、mouseup 和 click 回调,最后完成渲染并呈现下一帧;总延迟为三部分之和。
原创示意图,依据原文的交互生命周期绘制。条块仅表示先后关系,并非真实测量结果;每个阶段的长度都可能变化。

例如,用户输入时,主线程可能正在执行阻塞任务。浏览器必须等这些任务结束,才开始运行 pointerup、mouseup 和 click 的事件处理函数,随后再进行渲染、绘制,最后呈现下一帧。

虽然可以针对每个单独的 event 条目计算阶段耗时,但原文建议把同一帧内相关的多个 event 条目一起分析。这样才能正确区分延迟发生在开始处理之前、事件处理期间,还是所有处理函数结束之后。web-vitals JavaScript 库就是按这样的方式计算这些阶段。

三个阶段的和就是总交互延迟。任何一部分都可能拖慢响应,因此要分别检查,而不能只关注事件处理函数。

识别并减少输入延迟

输入延迟是交互开始后的第一段等待。页面上的其他活动可能把它拉长,例如主线程正在加载、解析、编译脚本,处理 fetch 的结果,运行定时器回调,或者处理短时间内连续发生、相互重叠的其他交互。

无论是哪种原因,目标都是尽可能缩短这段等待,让事件回调尽早开始执行。更详细的检查方法见优化输入延迟。

页面启动时,脚本求值如何产生长任务

页面启动阶段对交互能力尤其关键。浏览器已经把内容显示出来,并不意味着加载已经完成。网站要达到完整可用状态可能还需要不少资源,而用户很可能在这段时间里就开始操作。

脚本求值是启动阶段输入延迟的一种来源。JavaScript 文件从网络下载完成后,浏览器仍要解析脚本、检查语法、编译成字节码,最后再执行。脚本足够大时,这些工作可能在主线程上形成长任务,使浏览器暂时无法响应用户输入。

要让页面在加载过程中保持灵敏,需要降低这类长任务出现的概率。相关原理见脚本求值与长任务。

优化事件回调

输入延迟只是 INP 的一部分。交互开始被处理后,对应的事件回调也应尽快完成。

经常把执行机会让回主线程

最通用的建议是:让事件回调少做一些事情。但有些交互逻辑本来就很复杂,能删除的工作有限。此时可以把回调中的工作拆成多个独立任务,避免它们连成一个长任务、一直占住主线程。这样,其他正在等待的交互就有机会更早执行。

setTimeout 是拆分任务的一种方式,因为交给它的回调会在一个新任务中执行。你可以直接使用它,也可以把“让出执行机会”的过程封装成更方便调用的函数。详见优化长任务。

先拆分任务通常比完全不让出执行机会更好;但还可以更精细:在完成用户界面更新所需的工作之后立即让出,把渲染所需的执行机会尽早交回浏览器。

让渲染工作更早发生

更进一步,可以重新安排事件回调,只在其中执行下一帧必须使用的视觉更新逻辑,把其他工作延后到之后的任务。这样既能缩短回调,也能避免已经准备好的界面更新继续等待非关键代码。

以富文本编辑器为例:用户输入文字时,编辑器需要更新文本和格式,还可能更新字数、检查拼写,并保存内容,防止用户离开后丢失工作。这四项工作都可能需要做,但只有第一项必须在下一帧呈现之前完成:

  1. 把用户输入更新到文本框,并应用必要的格式。
  2. 更新界面上的字数。
  3. 执行拼写检查。
  4. 把最新修改保存到本地或远端数据库。

原文给出的示意代码如下:

textBox.addEventListener('input', (inputEvent) => {
  // 立即更新界面,让下一帧尽早显示用户的修改。
  updateTextBox(inputEvent);

  // 在 requestAnimationFrame 回调中再排一个任务,
  // 将其他工作推迟,避免它们直接阻塞下一帧。
  requestAnimationFrame(() => {
    setTimeout(() => {
      const text = textBox.textContent;
      updateWordCount(text);
      checkSpelling(text);
      saveChanges(text);
    }, 0);
  });
});

把非关键更新放到下一次绘制之后处理,可以减少当前交互的处理时长,进而降低总延迟。在 requestAnimationFrame() 中嵌套 setTimeout() 看起来有些绕,但这是原文提供的一种跨浏览器写法,目的是避免非关键代码继续占住下一帧之前的执行时间。

编辑补充:这是一段调度示例,textBox 和四个业务函数都需由应用提供,不能单独运行。定时器延迟为 0 不代表精确计时,实际绘制仍由浏览器调度。连续输入可能排入多批后台工作;真正的保存逻辑还要自行处理顺序、失败、去重和页面退出等情形。这里没有实现持久化保证。

避免布局抖动

布局抖动也常被称为强制同步布局。典型情形是:JavaScript 修改了样式,随即又在同一个任务中读取依赖布局的值。为了返回最新结果,浏览器不得不立刻同步执行本来可以稍后进行的布局计算。多种 DOM 属性读取都可能触发这种情况。

原文配图使用 Chrome DevTools 的 Performance 面板展示这类开销:涉及布局抖动的调用栈区域,常见 Recalculate Style 或 Layout 标签,右上角可能有红色三角标记。不同版本的 DevTools 展示方式可能不同。

web.dev 原始 Chrome DevTools Performance 面板截图,以箭头标出调用栈中 Layout 和 Recalculate Style 任务。
web.dev 原始 DevTools 性能追踪截图,未修改;作者 Jeremy Wagner、Philip Walton、Barry Pollard,CC BY 4.0。它是原文的说明截图,不是本文测量结果。原图来源。

问题不在于页面永远不能计算布局,而在于“写入后立即读取”的顺序迫使计算提前发生,阻止浏览器等到事件回调结束后再统一处理。进一步阅读:避免庞大复杂的布局与布局抖动。

尽量缩短呈现延迟

呈现延迟从交互的事件回调执行结束开始,到浏览器呈现出包含视觉变化的下一帧为止。即使 JavaScript 回调已经很短,接下来的渲染工作仍可能很重。

减少 DOM 规模

DOM 较小时,渲染工作通常能更快完成。DOM 变大,渲染所需工作也往往增加;两者并不是简单的线性关系,但大型 DOM 通常比小型 DOM 更难处理。

这会影响两个时刻:一是初次渲染,浏览器要为大量节点生成页面的初始状态;二是用户交互引发更新时,庞大的 DOM 可能显著提高更新成本,让下一帧更晚出现。

有时 DOM 规模无法大幅缩减。可以尝试减少不必要的嵌套,或在用户真正需要时再添加部分 DOM,让初始 DOM 更小,但这些方法也有适用限度。详见DOM 规模与交互能力。

用 content-visibility 延后屏外内容的渲染

CSS 属性 content-visibility 可以帮助限制页面加载和用户交互时的渲染工作。它允许浏览器对暂时不需要显示的内容跳过部分渲染,待内容接近视口等合适时机再处理。使用时需要实践和测量,但如果它确实减少了渲染耗时,也就可能改善 INP。参见content-visibility:提升渲染性能的 CSS 属性。

留意 JavaScript 渲染 HTML 的成本

只要页面中有 HTML,浏览器就需要解析它。HTML 被解析成 DOM 后,还要计算样式、进行布局,再把结果渲染出来。这些成本无法消失,但生成和传递 HTML 的方式会影响浏览器安排工作的能力。

服务器发送 HTML 时,内容以流的形式逐块到达。浏览器可以边接收边解析,分批渲染。这样的处理方式会在页面加载期间自然、周期性地让出执行机会,开发者无需额外安排。

另一种常见方式是先返回很少的 HTML,然后由 JavaScript 填充主要内容,后续内容更新也由交互驱动。这通常被称为单页应用(SPA)模式。

它的一个代价是:客户端既要执行生成 HTML 的 JavaScript,又要解析生成的 HTML 并完成相关渲染。原文强调,大批客户端生成工作会减少浏览器在关键处理路径上让出的机会,因而可能推迟视觉更新。

即使网站不是 SPA,也常常会在交互后通过 JavaScript 渲染部分 HTML。少量更新通常没有问题;真正需要警惕的是一次性渲染大量 HTML,把下一帧明显拖后。理解这类成本,才能判断客户端渲染如何影响响应能力。详见客户端 HTML 渲染与交互能力。

持续测量,逐步改进

INP 优化是一个反复迭代的过程。修好真实用户遇到的一项慢交互之后,尤其是在交互丰富的网站中,你往往还会发现下一项需要处理的问题。

关键是持续推进。随着问题逐个解决,页面响应可以逐渐达到令用户满意的水平。新功能也会带来新的交互路径,因此同样的测量、复现、拆分和优化流程可能需要再次进行。投入这些时间,是为了让真实使用中的体验更好。

来源与许可:本文译自 web.dev 原文。除原页另有说明的部分外,正文依 CC BY 4.0 授权,代码示例依 Apache License 2.0 授权,完整许可文本随稿附上(Apache 2.0 文本);本稿进行了中文翻译、术语整理及已标明的编辑校订。原文头图来自 David Pisnoy / Unsplash,并依 Unsplash 许可修改;本稿未复用该头图;原文中三张 CC BY 4.0 说明图已原样保留、逐图标明来源,另有一张原创三阶段概览图。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容