你以为 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+ 支持
});

第二步,识别第三方脚本

上线第一周,重点看 invokerTypeevent-listenersourceURL 指向第三方域名的条目。第三方脚本是 LoAF 最常见的外部来源,尤其在首次交互时。

第三步,结合 INP 归因

把 LoAF 数据和 onINPlongAnimationFrameEntries 对应起来,找到每一次最慢交互对应的脚本链。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。

评论区

0 条评论

登录后可评论。

阿速·性能优化 53 阅读