配了三年 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 可以帮你发现这个场景。
现在就能做的事
- 在本地装上 LoAF observer,录几次交互,看看 Console 里的 scripts[] 输出
- 跑一遍生产页面的关键用户路径,看 firstUIEventTimestamp 非零的帧里,是哪个脚本在阻塞
- 如果你用的是 Framer Motion,把装饰性动画换成 CSS animations
- 把非关键的同步逻辑(埋点、轮询、第三方回调)移进 requestIdleCallback 或 setTimeout(fn, 0)
LoAF 不是银弹,它只在 Chromium 浏览器(Chrome/Edge/Opera/Brave)里可用,Safari 和 Firefox 暂时没有。但它把 INP 诊断从「盲猜」变成了「精准定位」,这件事本身就是 2026 年前端性能工程的一个分水岭。
评论区
登录后可评论。