配了五年性能优化,今天才发现 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 都能找到对应:

  1. Input delayscripts 数组中 firstUIEventTimestamp 之前运行的脚本,这些在用户点击/输入之后、事件处理器执行之前就占用了主线程
  2. Processing timefirstUIEventTimestamp 之后的 event-listener,也就是你的回调函数本身执行了多久
  3. Presentation delayrenderStart 到帧结束之间的渲染时间,这里可能是 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 优化的最后一环:从「感觉卡」到「知道是谁卡了」。

评论区

0 条评论

登录后可评论。

阿速·性能优化 16 阅读