配了三年 INP 优化,每次用户说卡我都只能靠猜——今天 LoAF 把这个黑盒彻底拆开了

配了三年 INP 优化,每次用户说卡我都只能靠猜——今天 LoAF 把这个黑盒彻底拆开了

你有没有过这种经历:用户说「页面好卡」,你打开 Lighthouse,分数绿得发亮。你打开 DevTools Performance 面板,录了半天,看到了一个 200ms 的 Task,但点进去只知道是「某个脚本」,不知道具体是哪个函数,更不知道是谁调用的。

INP 已经替代 FID 成了 Core Web Vital 金标准,Google 把 2026 年 5 月设为 INP 全面上线的死线,可我们诊断慢交互的方式还停留在「感觉这个脚本该背锅」这个层面。

LoAF 改变了这件事。

LoAF 是什么

Long Animation Frames API,Chrome 123+ 内置,直接告诉你「哪个脚本、哪个函数、哪一行」造成了帧延迟。

在这之前,Long Tasks API 只能告诉你「主线程阻塞了 50ms 以上」,但它不告诉你是谁,连函数名都没有。

LoAF 的 entry 包含这些字段:

  • duration:帧总时长,超过 50ms 才会生成 entry
  • blockingDuration:真正在阻塞主线程的时长(减去 50ms 阈值),这个数字如果很高,说明瓶颈在 JavaScript;如果接近 0,说明瓶颈在渲染
  • renderStart:渲染开始的时间点,之前是脚本执行阶段,之后是 style/layout/paint
  • styleAndLayoutStart:浏览器开始同步 style + layout 的时间,renderStart 到这里之间是 requestAnimationFrame 回调
  • firstUIEventTimestamp:如果有用户交互,这是交互到达的时间戳,非零说明这个帧里用户在等待
  • scripts[]:每个运行超过 5ms 的脚本,含 sourceURL、sourceFunctionName、invokerType、forcedStyleAndLayoutDuration

最后这个 forcedStyleAndLayoutDuration 是关键:它记录了脚本触发了多少次同步 layout(读 offsetHeight、getBoundingClientRect 然后写 DOM 的经典反模式)。

真实案例:一个 Cookie 对话框拖慢了所有点击

Google Codelabs 的实测场景最能说明问题。大多数网页的第一次交互是 Cookie 同意对话框,用户点「接受」之后,后续的点击全变慢了。

原因:Cookie 脚本在用户点「接受」时同步执行了大量逻辑,导致后续事件处理器排队等待。

没有 LoAF 之前,你只能猜测是哪个脚本。用 LoAF 之后:

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 50) {
      console.log("LoAF:", {
        duration: entry.duration,
        blockingDuration: entry.blockingDuration,
        scripts: entry.scripts.map(s => ({
          fn: s.sourceFunctionName,
          url: s.sourceURL,
          forcedLayout: s.forcedStyleAndLayoutDuration
        }))
      });
    }
  }
});
observer.observe({ type: "long-animation-frame", buffered: true });

在 Console 里直接打印出是哪个脚本的哪个函数,耗时多少,有没有触发同步 layout。

生产环境怎么用:web-vitals attribution build

本地 DevTools 能录,生产环境靠 Real User Monitoring

Google 官方的 web-vitals 库有 attribution 版本,自动把 LoAF 和 INP 测量关联起来:

import { onINP } from "web-vitals/dist/web-vitals.attribution.js";

onINP(({ value, attribution }) => {
  // attribution 中包含了与这次 INP 交互重叠的 LoAF 数据
  console.log("INP:", value, "ms");
  console.log("LoAF entries:", attribution.longAnimationFrameEntries);
}, { reportAllChanges: true });

reportAllChanges 开启后,每次有新的最慢交互都会上报,而不是只上报一次。

LoAF 踩坑指南:五个常见场景

Mintec 在三个真实 Next.js 项目上用 LoAF 诊断 INP,发现 60%~70% 的 LoAF entry 来自同一两个动画脚本。典型场景:

1. 第三方脚本刚好在用户点击时运行
Cookie 同意框、评论组件、实时聊天 widget,它们的初始化脚本经常在用户第一次交互时触发。把这些脚本延迟加载,或者用 Partytown 跑进 Web Worker。

2. Framer Motion/GSAP 阻塞主线程
Mintec 实测:Framer Motion 完整包增加 46KB,每次 active 动画额外增加 30~50ms INP。用 CSS animations 替代装饰性动画,INP 从 268ms 降到 152ms。

3. 事件处理器里做了太多事
表单校验 + 状态更新 + 埋点全写在一个 addEventListener 里。把埋点和非关键逻辑移进 requestIdleCallback:

button.addEventListener("click", () => {
  showConfirmation(); // 紧急:立刻响应
  requestIdleCallback(() => sendAnalytics("cta_clicked"), { timeout: 2000 });
});

4. 读写了 layout 属性然后又写 DOM
读 offsetHeight → 写 className → 读 offsetHeight,这是经典的 layout thrashing。LoAF 的 forcedStyleAndLayoutDuration 会直接标出来。

5. React hydration 期间
大型组件树的 hydration 在用户第一次交互前还没完成,这时点击触发的交互会有巨大的 input delay。LoAF 的 firstUIEventTimestamp 可以帮你发现这个场景。

现在就能做的事

  1. 在本地装上 LoAF observer,录几次交互,看看 Console 里的 scripts[] 输出
  2. 跑一遍生产页面的关键用户路径,看 firstUIEventTimestamp 非零的帧里,是哪个脚本在阻塞
  3. 如果你用的是 Framer Motion,把装饰性动画换成 CSS animations
  4. 把非关键的同步逻辑(埋点、轮询、第三方回调)移进 requestIdleCallback 或 setTimeout(fn, 0)

LoAF 不是银弹,它只在 Chromium 浏览器(Chrome/Edge/Opera/Brave)里可用,Safari 和 Firefox 暂时没有。但它把 INP 诊断从「盲猜」变成了「精准定位」,这件事本身就是 2026 年前端性能工程的一个分水岭。

评论区

0 条评论

登录后可评论。

阿速·性能优化 12 阅读