前端知库

INP 很高时,如何定位到底是哪一段交互变慢?

从输入延迟、事件处理和呈现延迟三个阶段拆解 INP,结合 Event Timing、PerformanceObserver 和 DevTools 设计可复现、可验证的交互性能诊断流程。

参考答案

简答

  • INP 衡量交互到下一次绘制的延迟,≤200ms 为良好,>500ms 为较差,按 75 分位统计,2024 年 3 月取代 FID。
  • 一次交互分为输入延迟、处理时间和呈现延迟三段,要先判断慢在哪段。
  • 输入延迟高时减少主线程争用,处理时间高时缩小单次更新,呈现延迟高时检查样式、布局和绘制。
  • Event Timing 需要特性检测;RUM 按交互类型和分位数上报,不只看平均值。

用户点击筛选按钮后,页面过了半秒才显示结果。把所有 JavaScript 都压到 50 毫秒以内并不能保证体验良好,因为浏览器可能先忙于处理其他任务,事件处理结束后又花很久做样式、布局和绘制。INP(Interaction to Next Paint)关注交互开始到下一次呈现之间的完整时间,诊断时必须把这段时间拆开。

INP 在 2024 年 3 月取代 FID 成为 Core Web Vitals 指标。评估标准按页面访问的第 75 百分位统计:≤ 200 毫秒为良好,200 到 500 毫秒需要改进,超过 500 毫秒为较差。单次访问内,交互少于 50 次时取最慢的一次,交互更多时剔除少量极端值。

一次交互包含三个阶段

  1. 输入延迟:浏览器收到用户操作后,等待主线程可以运行事件处理的时间。
  2. 处理时间:事件监听器及其同步调用执行的时间。
  3. 呈现延迟:处理结束后,浏览器完成样式计算、布局、绘制并提交下一帧的时间。

长任务可能导致第一阶段变长;事件处理器可能触发大量组件更新;DOM 变化又可能让第三阶段变长。只看点击回调的耗时,无法解释后两种情况。

用 Event Timing 观察交互候选

JavaScript
function observeInteractions(onInteraction) {
  if (!('PerformanceObserver' in window)) return () => {};
  const types = PerformanceObserver.supportedEntryTypes ?? [];
  if (!types.includes('event')) return () => {};

  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (!Number.isFinite(entry.interactionId) || entry.interactionId === 0) continue;
      onInteraction({
        name: entry.name,
        startTime: entry.startTime,
        duration: entry.duration,
        interactionId: entry.interactionId,
        processingStart: entry.processingStart,
        processingEnd: entry.processingEnd,
        // presentationTime 并非所有实现都提供
        presentationTime: entry.presentationTime ?? null,
      });
    }
  });

  observer.observe({ type: 'event', buffered: true, durationThreshold: 16 });
  return () => observer.disconnect();
}

不同浏览器对 Event Timing 字段、事件类型和缓冲行为的支持会变化,代码必须做特性检测。duration 是浏览器记录的事件持续时间,不能简单当成“业务函数执行时间”;同一交互可能产生多个事件条目,真实 RUM 计算需要使用规范和官方库定义的聚合方式。示例用于理解诊断字段,不应直接用一个条目替代完整 INP 计算。

可以先拆分单个事件条目的阶段,帮助定位,而不自行实现整页 INP 聚合:

JavaScript
function splitEventTiming(entry) {
  const { startTime, processingStart, processingEnd, duration } = entry;
  if (![startTime, processingStart, processingEnd, duration].every(Number.isFinite) ||
      startTime < 0 || processingStart < startTime || processingEnd < processingStart || duration < 0) {
    return null;
  }
  return {
    inputDelay: processingStart - startTime,
    processingDuration: processingEnd - processingStart,
    // duration 会被量化,差值只能作为估算;避免显示负延迟。
    presentationDelayEstimate: Math.max(0, startTime + duration - processingEnd),
  };
}

例如 startTime=100、processingStart=130、processingEnd=150、duration=80,得到约 30 毫秒输入等待、20 毫秒处理和 30 毫秒呈现等待。真实 duration 有精度限制,不能据此报告微秒级差异。连续拖动或滚动的流畅度也不能只靠 INP 判断;INP 主要关注点击、轻触和键盘等交互,动画帧问题另用性能时间线诊断。

先按交互类型复现,再看调用栈

开发环境先关闭无关扩展,使用固定数据和固定设备复现“点击筛选”“输入搜索”“打开菜单”等具体动作。DevTools Performance 录制应覆盖动作前后的完整时间线,观察:

  • 事件开始前是否有长任务或大量脚本执行;
  • 事件处理器是否同步解析、排序或序列化大数据;
  • 框架提交是否更新了过多节点;
  • 样式、布局、绘制是否占据主要时间;
  • 是否因为同步读取布局属性触发强制布局;
  • 下一帧是否被图片解码、第三方脚本或动画占用。

先确定阶段,再决定优化方向。单纯增加防抖可能减少事件数量,却无法修复一次点击后的昂贵渲染;把任务丢进 setTimeout 也可能只是把呈现延迟推后。

输入延迟高:减少主线程争用

输入前的长任务通常来自初始化、第三方脚本、批量数据处理或大段同步渲染。可按任务拆分、延迟非关键初始化、使用 Worker 处理纯计算,或在空闲时段安排低优先级工作。不能把所有工作都搬进 Worker:DOM 和大部分框架渲染仍需要主线程,序列化大对象本身也有成本。

避免同步 XHR 等阻塞操作,也不要在用户每次按键时执行全量搜索。await fetch 本身会让出执行,不是“网络等待期间一直占住主线程”;但结果返回后的解析、排序和渲染仍可能产生长任务。输入值可以先更新轻量 UI,再取消旧请求;服务端结果返回后还要验证请求序号,避免旧结果覆盖新输入。

如果点击后立即显示加载反馈,下一帧可以很快出现,即使接口两秒后才完成,INP 仍可能良好。结果完成时间是另一个业务指标,需要分别测量;不能用低 INP 证明端到端操作已经足够快。

处理时间高:缩小一次更新的工作量

事件回调应尽快完成必要状态更新,把不影响下一帧的工作拆开。列表筛选可以先更新加载状态,再分页或虚拟化渲染;复杂排序可以在 Worker 中执行,或在服务端查询。框架中要检查组件是否因为父状态变化而全部重渲染,使用稳定 key、细粒度状态和合理的 memo 化,但不要盲目给每个组件套缓存。

如果事件来自连续输入或拖动,应使用节流和合并更新;如果是必须响应的单次点击,重点是减少同步计算和 DOM 更新。任何优化都要重新录制,确认交互结果没有被延迟到下一次任务之后。

呈现延迟高:检查样式、布局和绘制

事件处理完成后仍然慢,常见原因包括大量 DOM、复杂选择器、布局抖动、图片解码、阴影和透明叠加。把 DOM 写入和读取交错执行会触发强制布局:

JavaScript
// 容易产生布局抖动:写入后立即读取,再写入
for (const item of items) {
  item.style.width = `${nextWidth}px`;
  const height = item.offsetHeight;
  item.style.transform = `translateY(${height}px)`;
}

可以先批量读取需要的尺寸,再批量写入;对不需要立刻参与布局的内容使用合适的容器隔离、虚拟化或延迟渲染。content-visibility、合成层和 will-change 不是万能开关,滥用会增加内存或造成合成压力,必须通过录制验证。

RUM 中不要只上报平均值

INP 是长尾体验指标,平均值会掩盖低端设备和特定交互的问题。生产监测应按页面、交互名称、设备类别、连接类型和版本计算分位数,并记录输入到处理开始、处理结束到呈现的大致阶段。采集需采样并脱敏,交互名称使用固定白名单,不能把输入框内容或完整 URL 当作标签。

开发环境的单次录制与 RUM 的统计用途不同:前者用来定位调用栈,后者用来判断问题是否普遍以及修复是否有效。发布新版本后应比较同一页面、同一交互和相近设备分布,避免把流量结构变化误判为代码优化。

兼容性和降级

Event Timing、presentationTime 和交互观察能力并非所有浏览器都完整支持。缺少这些 API 时,仍可以使用 DevTools、Long Task 观察、用户计时标记和框架性能工具做部分诊断。性能采集脚本失败不能阻断业务页面;关键交互应优先保持可用,再逐步增强数据采集。

参考资料