用户说页面卡,你只盯着 Lighthouse 的分数——今天才发现真正的瓶颈根本不在那个数字里,在浏览器自己会告诉你的那件事里

用户说页面卡,你只盯着 Lighthouse 的分数——今天才发现真正的瓶颈根本不在那个数字里,在浏览器自己会告诉你的那件事里。


配了三年性能监控,你一定见过这个场景:Lighthouse 跑了个 98 分,用户还在骂「点一下要等两秒才动」。INP(Interaction to Next Paint)明明已经上了 Core Web Vitals,但 Lighthouse 绿了不代表你的页面真的跟手。你缺的从来不是那个总分数——是你不知道哪一次具体的交互在拖后腿,以及拖的是哪个阶段。

Chrome 的 Event Timing API 终于把这件最该有答案的事说清楚了。

一行代码,把最慢的那次交互抓出来

Event Timing API 底层就是 PerformanceEventTiming,INP 就是从这跑出来的数据。但它给你的不是总分,是每一次交互的完整时间戳:

let worstINP = { duration: 0, interactionId: 0 };

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === "event") {
      // 这次交互的输入延迟(主线程忙了多久才跑你的代码)
      const inputDelay = entry.processingStart - entry.startTime;
      // 这次交互的完整时长:输入延迟 + 处理时间 + 渲染时间
      if (entry.duration > worstINP.duration) {
        worstINP = { duration: entry.duration, interactionId: entry.interactionId };
      }
    }
  }
}).observe({ type: "event", buffered: true });

跑完这个,用户说「点按钮等了」——你直接知道是哪类交互、延迟了多少毫秒、Lighthouse 根本没测到。不是玄学,是可操作的真实数据。

交互延迟的三段式拆解

Event Timing 把一次交互的卡顿拆成了三个阶段,每个阶段都对应一个具体的原因:

输入延迟(inputDelay)entry.processingStart - entry.startTime。主线程被其他任务占着,你的代码还没机会跑。根因:上一个长任务还没跑完,或者浏览器在处理别的事。

处理时间(processingTime)entry.processingEnd - entry.processingStart。你的事件处理器实际跑了多久。根因:JS 执行太重、React 重新渲染了一次完整的虚拟 DOM。

呈现延迟(presentationDelay)entry.startTime + entry.duration - entry.processingEnd。事件处理完了,浏览器还要等多久才把帧画出来。根因:触发了重排或重绘,或者渲染管线里有别的瓶颈。

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === "event") {
      const inputDelay = entry.processingStart - entry.startTime;
      const processingTime = entry.processingEnd - entry.processingStart;
      const presentationDelay =
        entry.startTime + entry.duration - entry.processingEnd;
      console.log({
        type: entry.name,
        inputDelay: Math.round(inputDelay),
        processingTime: Math.round(processingTime),
        presentationDelay: Math.round(presentationDelay),
        total: Math.round(entry.duration),
      });
    }
  }
}).observe({ type: "event", buffered: true });

高 INP 是哪个阶段拖的后腿,三行数字直接定位。不是靠经验猜,是测量出来的。

104ms 这个门槛,你调过吗

Event Timing API 默认只暴露 duration ≥ 104ms 的事件(8 的倍数,100ms 安全舍入)。但如果你想看所有交互——包括那些「感觉还行但其实有点慢」的——可以把 threshold 调低:

// durationThreshold: 0 就能看到所有交互,包括 < 104ms 的
observer.observe({ type: "event", buffered: true, durationThreshold: 0 });

web-vitals 5.3.0 的 onINP() 默认 threshold 是 40ms,低于「Good」线(200ms)的 INP 不报,但会降级到首次输入延迟作为兜底。知道这个参数能帮你做更精细的 RUM 监控。

interactionCount:你一直漏掉的那个数据

Event Timing 的 entry 里还有个被低估的属性叫 interactionCount,代表页面上发生的交互总次数。高交互量页面的 INP 测的是 p98,低流量页面测的是 p95。如果你的 INP 是 380ms 但页面只有 5 次交互——那其实只有一次交互很差劲;如果同一时间有 200 次交互,那就是大范围的问题。

// interactionCount 帮你判断 INP 劣化是孤立的还是系统性的
const interactions = performance.interactionCount || 0;

配合 worstINP.duration 一起看,你能判断:这是个例还是全局问题?该不该从单次优化入手还是从架构层入手?

怎么把这些数据用到生产环境

最直接的方式是接 web-vitals:

import { onINP } from "web-vitals";

onINP(({ value, entries }) => {
  // value = 页面级 INP(p75)
  // entries = 导致这个 INP 的那次交互的原始 PerformanceEventTiming 数据
  const [{ processingStart, processingEnd, duration }] = entries;
  console.log(`INP: ${value}ms`);
  sendToAnalytics({ metric: "INP", value, duration });
});

或者用 document.onvisibilitychange 捕获用户离开前的最终 INP:

let finalINP = 0;
new PerformanceObserver((list) => {
  for (const e of list.getEntries()) {
    if (e.duration > finalINP) finalINP = e.duration;
  }
}).observe({ type: "event", buffered: true });

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    sendToAnalytics({ metric: "worstINP", value: Math.round(finalINP) });
  }
});

知道了瓶颈在哪,怎么修

三段式拆解之后,每一段都有明确的优化路径:

输入延迟长 → 用 scheduler.yield() 把长任务切成片,让交互请求插队进来
处理时间长 → 代码分割、按需加载、避免同步执行的 heavy computation;考虑 Web Worker 把计算卸到主线程之外
呈现延迟长 → CSS 动画替 JS 动画;用 transform/opacity 代替 width/height;避免交互后触发强制重排

// 交互处理中主动让出主线程
button.addEventListener("click", async () => {
  await scheduler.yield(); // 让交互结果先显示出来
  await heavyComputation(); // 重活后面跑
});

结论

Lighthouse 是模拟环境,Event Timing API 是真实用户。两者给的不是同一道题的答案——Lighthouse 告诉你「页面在理想环境下能跑多快」,Event Timing 告诉你「你的真实用户在哪些交互上卡了、卡在哪个阶段」。

一个 Lighthouse 分数不好看的页面可能用户实际体验还行;一个 Lighthouse 跑满分的页面,真实用户 INP 可能已经红了好几个月了。

花十分钟把 PerformanceObserver 接进你的 RUM 系统,下次用户说「卡」,你直接告诉他「是点击购物车那个按钮,输入延迟 280ms,根子在那个 300ms 的长任务」——这才是性能监控该有的样子。

评论区

0 条评论

登录后可评论。

阿速·性能优化 16 阅读