你以为 INP 达标就算快了?今天 LoAF 把这件事彻底说清楚了
你的页面 Lighthouse 分数很好看,INP 数值也在合格线以内,但用户就是反馈「点击之后要等一下才有反应」。这种情况在过去只能靠猜——是哪个脚本在捣鬼?是哪个回调占用了主线程?
现在 Chrome 123+ 自带了一个 API,能把这个问题精确到「哪一行代码、叫什么函数、阻塞了多久」。这就是 Long Animation Frames API,简称 LoAF。
01 LoAF 解决的是什么问题
Long Tasks API 在 2017 年问世的时候,解决了「主线程是不是卡了」的问题——只要任务超过 50 毫秒就给你记一笔。但它有个致命缺陷:只告诉你「卡了」,不告诉你「谁卡了」。
LoAF 继承了这个 50 毫秒的阈值,但把诊断能力往前推了一大步。它观察的是「帧」——确切地说,是从用户输入到下一帧渲染完整地之间的完整时间窗口。一帧超过 50 毫秒,LoAF 就记录一次,并且把这次帧里面所有在运行的脚本全部归因报告出来。
这个能力直接对应 INP(Interaction to Next Paint,2024 年 3 月正式成为 Core Web Vitals 指标)的诊断需求。INP 考核的是从用户交互到下一帧渲染的延迟,而 LoAF 记录的正是这个延迟的内部构成。
02 真实案例:Lighthouse 全过,实测 17 个站 INP 翻车
这个差距有多大?Webflow 性能工程师 Pravin Kumar 在 2026 年 5 月做了一个统计:他在该月做了 19 次 Webflow 站点审计,其中 17 次在真实用户的第 75 百分位 INP 数据中不合格。但这 19 个站的 Lighthouse 分数全部看起来没问题。
根本原因:实验室数据和真实用户数据之间的鸿沟。Lighthouse 在一个模拟网络环境下跑,你的真实用户可能在地铁里用着 4G,第三方标签管理器在首次交互时 fires,把主线程堵了 300 毫秒——这种事 Lighthouse 根本看不到。
LoAF 的价值正是在这里:让真实用户的帧数据第一次变得可采集、可分析、可归因。
03 脚本级归因:LoAF 给了哪些数据
这是 LoAF 和 Long Tasks 本质的区别。每条 LoAF 记录包含一个 scripts 数组,每个 script 条目里有:
invoker 和 invokerType——谁调用的这段代码。常见类型包括:
event-listener:浏览器事件回调(click、keyup、load)user-callback:平台 API 的用户回调(setTimeout、requestAnimationFrame)resolve-promise/reject-promise:Promise 的 then/catch 处理器classic-script/module-script:脚本文件执行
sourceURL 和 sourceFunctionName——哪个文件、哪个函数。LoAF API Playground 特别指出:用命名函数表达式替代匿名箭头函数,函数名才能被正确捕获。比如把 requestAnimationFrame(() => {}) 改成 requestAnimationFrame(function doAnimation() {}),归因数据就清晰得多。
forcedStyleAndLayoutDuration——这次脚本执行期间,强制同步布局和样式计算(forced synchronous layout)占了多少毫秒。这是 DOM thrashing 的直接证据:写-读-写循环触发 layout,即使用户感知不到。
windowAttribution——脚本运行在哪个 window(self/descendant/ancestor/same-page/other)。可以判断长任务是不是来自页面里的第三方 iframe。
来看一个完整的 LoAF 条目长什么样(来自 Request Metrics 的示例):
{
"blockingDuration": 74,
"duration": 128,
"renderStart": 271,
"styleAndLayoutStart": 271,
"firstUIEventTimestamp": 0,
"scripts": [
{
"duration": 16,
"executionStart": 253.3,
"forcedStyleAndLayoutDuration": 14,
"invoker": "#document.onDOMContentLoaded",
"invokerType": "event-listener",
"sourceURL": "https://example.com/js/main.js",
"sourceFunctionName": "onReady",
"windowAttribution": "self"
}
]
}
blockingDuration: 74 毫秒是真正的用户感知延迟——这段时间主线程无法响应输入。
04 怎么接:PerformanceObserver 和 web-vitals.js
接入代码非常简单,Chrome 123+ 原生支持,不需要任何 polyfill:
// 方式一:原生 PerformanceObserver(推荐)
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.duration:帧总时长(毫秒)
// entry.blockingDuration:真正阻塞输入的时长
// entry.scripts[]:这次帧里所有脚本的归因数据
console.log('LoAF:', {
duration: entry.duration,
blockingDuration: entry.blockingDuration,
scripts: entry.scripts.map(s => ({
invokerType: s.invokerType,
sourceURL: s.sourceURL,
sourceFunctionName: s.sourceFunctionName,
duration: s.duration,
forcedStyleAndLayoutDuration: s.forcedStyleAndLayoutDuration
}))
});
}
});
observer.observe({ type: 'long-animation-frame', buffered: true });
方式二是通过 Google 的 web-vitals.js 库,v5.0(2026 年 4 月 22 日发布)内置了 LoAF 集成,专门把 LoAF 数据和每个 INP 条目关联起来:
import { onINP } from 'web-vitals/attribution';
onINP(({ longAnimationFrameEntries }) => {
longAnimationFrameEntries.forEach(loaf => {
// loaf.scripts 就是导致这次 INP 超过阈值的脚本列表
console.log('INP 的 LoAF 归因:', loaf.scripts);
});
});
05 实战诊断例子:hover 菜单堵了 380 毫秒
LoAF Playground 提供了一个实际案例:一个 Webflow 站点,导航菜单 hover 动效在真实用户那里触发了 380 毫秒的 INP。工程师花了三周排查,Lighthouse 没有复现,Performance 面板也没抓到(因为它是真实用户在地铁里触发的,不是在实验室)。
接了 LoAF 之后,一次生产会话就找到了:webflow.js 里的 hover handler,在 ix2 交互处理中对菜单做了读写交替操作,触发了 forced synchronous layout,累计阻塞 380 毫秒。
这不是孤例。RUMvision 的分析显示,在真实用户环境下,Mixpanel、Segment、PostHog 等第三方分析脚本在首次交互时 fires 并阻塞主线程,是最常见的高 blockingDuration 来源之一。
06 生态现状:谁在用 LoAF 数据
LoAF 已经在 RUM 产品里广泛落地:
- web-vitals.js v5+:INP 条目直接带 LoAF 脚本归因
- Vercel Speed Insights:2026 年 5 月 1 日上线 LoAF 捕获
- SpeedCurve:同周支持
- RUMvision:提供 LoAF hostname/script/function/window 四级下钻
- DebugBear:集成 LoAF 做 INP 诊断
- Request Metrics:内置 LoAF 报告,支持在加载瀑布图里直接看脚本归因
Chrome DevTools 本身也在跟进——从 2024 年开始,Web Vitals 扩展程序里的 LoAF 功能已迁移到 DevTools 原生面板,Performance 面板里可以直接看到 LoAF 条目。
07 三步上手
第一步,接入观测
在页面底部加 PerformanceObserver 观测,或者直接升级到 web-vitals.js v5。阈值建议从 75 毫秒起(超过 75ms 才上报),避免噪音:
observer.observe({
type: 'long-animation-frame',
buffered: true,
durationThreshold: 75 // Chrome 133+ 支持
});
第二步,识别第三方脚本
上线第一周,重点看 invokerType 是 event-listener 且 sourceURL 指向第三方域名的条目。第三方脚本是 LoAF 最常见的外部来源,尤其在首次交互时。
第三步,结合 INP 归因
把 LoAF 数据和 onINP 的 longAnimationFrameEntries 对应起来,找到每一次最慢交互对应的脚本链。RUM 工具会自动做这个关联,但如果你自己搭观测,可以参考这个逻辑:
onINP(({ longAnimationFrameEntries, value }) => {
if (longAnimationFrameEntries.length > 0) {
const worst = longAnimationFrameEntries.reduce((a, b) =>
a.blockingDuration > b.blockingDuration ? a : b
);
// worst.scripts[0] 就是这次 INP 的头号罪犯
report({ inp: value, topScript: worst.scripts[0] });
}
});
总结
LoAF 解决的不是「页面快不快」的问题,而是「快不快怎么证明」的问题。INP 指标本身只给一个数字,LoAF 把这个数字拆开——让每一帧延迟都能追溯到具体的脚本、具体的函数、具体的操作。
Lighthouse 全过但用户还是觉得卡,这个矛盾从今往后有了一个标准化的诊断出口。Chrome 123+ 已经在稳定版里,你不需要装任何依赖,只需要一个 PerformanceObserver。
评论区
登录后可评论。