配了五年性能优化,今天才发现 Long Tasks 从来没告诉过你「哪一行代码慢了」——LoAF 把这件事彻底变了
每次用户说页面卡,你去 DevTools 抓 Long Task,看到的只有「某任务执行了 200ms」——但到底是哪一行代码,谁写的,什么时候触发的,一概不知。Chrome 123 把这件事用一个新的 API 彻底接上了。
Long Tasks 的两个致命缺陷
第一个问题:只报时长,不报原因。Long Tasks 的触发门槛是 50ms,但一个 40ms 的 handler 加上 70ms 的渲染,加起来是 110ms 的体验崩溃——Long Tasks 一条也不给你报。
第二个问题:没有脚本归因。你只知道「有个长任务」,不知道哪个脚本、哪个函数、更不知道第几行。
结果就是:优化靠猜,改完靠撞运气。
LoAF 来了:脚本级 INP 诊断
Long Animation Frames API(LoAF),Chrome 123+ 稳定支持,就是来解决这个问题的。它比 Long Tasks 多给了 7 个关键字段,把「卡了多久」变成「谁卡了」。
核心字段解读:
duration:帧总时长,超过 50ms 才入账blockingDuration:真正阻塞浏览器的毫秒数(自动减去 50ms 阈值)renderStart:渲染开始时间,之前全是脚本执行styleAndLayoutStart:样式计算开始,进一步分割阶段scripts[]:每个超过 5ms 的脚本条目——这里才是关键
scripts 数组里的每个条目包含:
sourceURL:哪个文件的哪一行functionName:哪个函数invokerType:触发的类型(setTimeout、事件回调、RAF 等)duration:这个脚本自己占多久forcedStyleAndLayoutDuration:因为它触发了多少次强制重排
三行代码开始诊断
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.log("LoAF:", entry.duration + "ms", entry.scripts);
}
}
});
abserver.observe({ type: "long-animation-frame", buffered: true });
这段代码抓出所有超过 50ms 的 LoAF,并把它们打印出来——包括每个脚本的来源文件和函数名。
LoAF × INP 三个阶段对应关系
INP(Interaction to Next Paint)的三个阶段,LoAF 都能找到对应:
- Input delay:
scripts数组中firstUIEventTimestamp之前运行的脚本,这些在用户点击/输入之后、事件处理器执行之前就占用了主线程 - Processing time:
firstUIEventTimestamp之后的event-listener,也就是你的回调函数本身执行了多久 - Presentation delay:
renderStart到帧结束之间的渲染时间,这里可能是 JS 写完 DOM 之后浏览器算布局的时间
这样就把 INP 的 200ms 拆成了三笔账,清清楚楚。
三个真实场景案例
案例一:Framer Motion 动效
一个产品列表页面,滚动时帧率骤降。LoAF 抓到一个 287ms 的帧:Framer Motion 占 134ms(layout 计算)+ 89ms(style 计算)。替换为 CSS transform + opacity 之后,LoAF 消失,INP 从 287ms 降到 42ms。
案例二:第三方脚本
某电商网站的聊天组件在初始化时触发了一个 180ms 的 LoAF。LoAF 的 sourceURL 直接指向 cdn.chat-widget.com/bundle.js,归因到具体域名——而不是「不知道哪个脚本」。
案例三:滚动监听交替读写
一个需求是「滚动时固定侧边栏高度等于主内容高度」,代码这么写:
window.addEventListener("scroll", () => {
const height = element.offsetHeight; // 读
element.style.height = height + "px"; // 写
});
这就是经典的「读写交替」——每次 scroll 都会触发 forced synchronous layout。LoAF 把这类问题归因到 forcedStyleAndLayoutDuration 字段,一目了然。
三个坑
坑一:只有 Chromium 支持
LoAF 目前只有 Chrome 123+ 和 Edge 123+ 支持。Safari 和 Firefox 都没有,需要 feature detection 降级:
if ("PerformanceObserver" in window) {
// LoAF 逻辑
}
坑二:scripts[] 只记录超过 5ms 的脚本
LoAF 不会记录所有脚本,只会记录 duration 超过 5ms 的。如果一个脚本只跑了 3ms,它不会出现在归因里——但它可能通过累积次数拖慢帧率。
坑三:跨域脚本和 Worker 不归因
跨域 iframe、Service Worker、Web Worker 的脚本不会出现在 scripts[] 里。如果你用 iframe 嵌入了第三方组件,那个组件的脚本不会归因——只能看到它阻塞了渲染,但看不到具体代码。
三步下一步
第一步:DevTools Performance 面板找 LoAF
Chrome DevTools → Performance → 录制一次用户交互(比如点击、输入、滚动)→ Interactions track 找超过 50ms 的 LoAF → 展开 scripts[] 看来源文件。
第二步:web-vitals/attribution 抓生产数据
import { onINP } from "web-vitals/attribution";
onINP(({ value, attribution }) => {
console.log("INP:", value, attribution.longestScriptSource);
});
这个库会把 LoAF 中最慢的脚本来源打包进你的 RUM 数据里。
第三步:建立 LoAF 画像
把超过 50ms 的 LoAF 用 navigator.sendBeacon() 发到你的数据服务,按页面维度聚合。持续监控两周之后,你会得到一个「这个页面最慢的三个脚本」清单——优化目标清晰了,INP 优化从玄学变成工程。
LoAF 补全了 INP 优化的最后一环:从「感觉卡」到「知道是谁卡了」。
评论区
登录后可评论。