登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

Long Animation Frames API 怎么定位卡顿脚本

来源:17golang原创

时间:2026-10-04 17:44:48 463浏览 收藏

热门推荐
漫画APP
动画内容聚合,热门资源快捷查看
立即下载

页面点击、滚动或动画突然卡一下时,Long Animation Frames API(LoAF)可以把超过 50ms 的长帧记录到性能时间线,并在 scripts 中给出贡献脚本的地址、入口函数、字符位置和耗时。定位时先按 blockingDuration 找最影响响应的长帧,再按 sourceURL + sourceCharPosition 聚合脚本;这会得到值得复查的候选,不应直接把耗时最长的一项等同于唯一根因。

要点速览
  • 先用 PerformanceObserver.supportedEntryTypes 检测 long-animation-frame,不要假定所有浏览器都支持。
  • 帧级先看 duration、blockingDuration 和 firstUIEventTimestamp,脚本级再看 duration、invoker 与 source* 字段。
  • sourceFunctionName 通常是脚本入口点,不是完整调用栈中的最慢子函数;跨域 iframe、Worker 和扩展代码也可能缺少归因。

先确认浏览器能不能提供 LoAF 数据

LoAF 仍不是所有浏览器都具备的通用能力。初始化监控前检查支持类型;不支持时保留基础交互指标、Long Tasks 或现场性能分析作为降级路径。这样不会因为一段实验性监控代码影响主业务。

const LOAF_TYPE = "long-animation-frame";

// 运行时检测支持范围,避免在不支持的浏览器中直接注册。
const supportsLoAF =
  "PerformanceObserver" in window &&
  PerformanceObserver.supportedEntryTypes.includes(LOAF_TYPE);

if (!supportsLoAF) {
  // 这里只记录能力缺失;业务逻辑必须继续正常运行。
  console.info("Long Animation Frames API is unavailable");
}

规范把长动画帧定义为持续时间超过 50ms 的帧。这个门槛由 API 决定,但你的上报门槛可以更高,例如先只保留 120ms 或 150ms 的严重样本,降低数据量。性能时间线的 LoAF 缓冲区容量有限,MDN 提醒最多保留 200 条,因此持续监控应优先使用 PerformanceObserver,而不是等页面结束时再一次性读取。

先采集长帧,再判断卡顿落在哪一层

遇到“点击后过一会儿才响应”的现场,先不要急着只看脚本文件。duration 表示整段长帧持续时间,blockingDuration 更接近主线程无法及时响应高优先级任务的阻塞量;firstUIEventTimestamp 大于 0 时,说明帧内处理过 UI 事件。renderStart 与 styleAndLayoutStart 则帮助判断时间是否大量落在渲染、样式和布局区域。

Long Animation Frame 条目中 duration、blockingDuration、scripts 与渲染时间点的静态结构图
图1:LoAF 条目的静态字段结构。先看长帧总量,再结合交互、脚本与渲染时间点判断卡顿层次;这是说明图,不是运行截图。
const SERIOUS_FRAME_MS = 120;
const recentLoAFs = [];

function observeLongAnimationFrames() {
  if (!PerformanceObserver.supportedEntryTypes.includes("long-animation-frame")) {
    return null;
  }

  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      // 先保留严重长帧,避免把所有 50ms 样本都送入后续分析。
      if (entry.duration  20) recentLoAFs.shift();
    }
  });

  // buffered 同时接收缓冲区内既有条目和后续新条目。
  observer.observe({ type: "long-animation-frame", buffered: true });
  return observer;
}

这段代码只保存诊断需要的字段,没有上报完整 DOM、用户输入或页面内容。生产采集还应设置抽样率、总量上限和数据保留周期,避免性能监控自己变成额外负担。

从 scripts 提取最值得复查的脚本

entry.scripts 中的每一项是 PerformanceScriptTiming。定位脚本身份时组合使用 sourceURL、sourceFunctionName 和 sourceCharPosition;判断调用来源时看 invoker 与 invokerType;判断成本时看 duration、forcedStyleAndLayoutDuration 和 pauseDuration。

PerformanceScriptTiming 的脚本身份、调用入口与成本字段静态分组图
图2:PerformanceScriptTiming 的静态字段分组。脚本身份、调用入口和成本信号组合起来,才能形成可复查的卡顿线索;这不是性能面板截图。
function normalizeScriptURL(rawURL) {
  try {
    // 去掉查询参数和片段,避免聚合键被版本参数拆散,也减少敏感信息进入日志。
    const url = new URL(rawURL, location.href);
    return `${url.origin}${url.pathname}`;
  } catch {
    return "inline-or-unknown";
  }
}

function rankScriptContributors(frames) {
  const totals = new Map();

  for (const frame of frames) {
    for (const script of frame.scripts) {
      const sourceURL = normalizeScriptURL(script.sourceURL);
      const key = [sourceURL, script.sourceCharPosition, script.sourceFunctionName].join("|");
      const current = totals.get(key) ?? {
        sourceURL,
        sourceFunctionName: script.sourceFunctionName || "anonymous-entry",
        sourceCharPosition: script.sourceCharPosition,
        invoker: script.invoker,
        count: 0,
        totalDuration: 0,
        totalForcedLayout: 0,
      };

      current.count += 1;
      current.totalDuration += script.duration;
      current.totalForcedLayout += script.forcedStyleAndLayoutDuration;
      totals.set(key, current);
    }
  }

  // 总执行时间优先,次数作为次级信号,找出高频且高成本的入口。
  return [...totals.values()].sort(
    (a, b) => b.totalDuration - a.totalDuration || b.count - a.count,
  );
}

// 调试时查看聚合结果;正式上报只发送必要字段和受控数量。
console.table(rankScriptContributors(recentLoAFs).slice(0, 10));

字符位置比只有文件名更有区分度,因为同一 bundle 可能包含多个入口。若部署时有 source map,可以在后端或调试工具中把 sourceCharPosition 映射回源码;不要在浏览器里假设压缩后的函数名长期稳定。

用字段组合判断 JavaScript、布局还是渲染

证据组合优先复查方向常见动作
脚本 duration 高,强制布局低事件回调、循环、序列化、同步计算拆分长任务、减少工作量、延后非关键逻辑
forcedStyleAndLayoutDuration 高脚本触发的同步样式和布局合并 DOM 读写、减少布局抖动
脚本总耗时不高,但帧 duration 高渲染、样式、布局或缺失归因比较 renderStart 与 styleAndLayoutStart,继续做现场剖析
pauseDuration 高alert、同步请求等暂停型操作移除同步阻塞 API
firstUIEventTimestamp 大于 0与用户交互重叠的长帧优先关联 INP 样本和交互入口

blockingDuration 不是简单等于 duration - 50。它会综合帧内超过 50ms 的长任务以及相关渲染成本,更适合用来排序“主线程有多长时间无法及时响应”。分析时保留 duration 与 blockingDuration 两个维度,不要只看一个数字。

归因为空或函数名不准时怎么继续

LoAF 的脚本归因有明确边界。MDN 说明,sourceFunctionName 指向脚本入口点,而不是完整调用栈里的最慢子函数;跨域 iframe、Web Worker、Service Worker 和扩展代码即使影响帧时长,也可能没有脚本归因。scripts 为空并不代表“没有 JavaScript 问题”,只代表当前条目没有可公开的脚本信息。

遇到缺口时按下面顺序复查:

  • 确认当前样本来自支持 long-animation-frame 的浏览器。
  • 检查卡顿是否主要发生在跨域 iframe、Worker 或第三方环境。
  • 用 renderStart 与 styleAndLayoutStart 判断是否偏向渲染成本。
  • 把脚本入口与真实交互场景关联,再进入本地性能分析寻找入口内部的慢函数。
  • 不支持 LoAF 时,用 Long Tasks、Event Timing 或开发者工具保留降级诊断。

修复后用相同维度反向验证

优化前后都使用相同浏览器范围、相同抽样规则和相同聚合键。验收时不要只看某一次长帧消失,而要观察高位样本中目标脚本的出现次数、累计 duration、累计强制布局时间和帧级 blockingDuration 是否一起下降。若脚本耗时下降但长帧仍在,说明瓶颈可能转移到了渲染或另一个入口。

  • 已运行时检测,不支持的浏览器不会报错。
  • Observer 持续消费条目,并限制内存中的样本数量。
  • 脚本聚合键包含地址与字符位置,不只按文件名统计。
  • 上报前去掉查询参数,避免泄露令牌、用户标识或实验参数。
  • 入口函数只作为线索,最终根因仍通过源码与现场剖析确认。
  • 优化后复测使用相同字段和门槛,结果才可比较。

相关问题

LoAF 和 Long Tasks API 有什么区别?

Long Tasks 关注单个长任务;LoAF 以动画帧为单位,还提供脚本归因和渲染时间点,更贴近用户看到的卡顿。多个不足 50ms 的任务也可能共同组成一个长帧。

为什么 scripts 里只有入口函数,没有最慢子函数?

完整调用栈的采集成本较高,API 报告的是脚本入口点。需要借助 source map 和现场性能分析继续深入。

只按 duration 最大值排序够吗?

不够。还要看 blockingDuration、出现次数、是否与 UI 事件重叠,以及强制布局时间,避免把偶发长帧误判成长期热点。

生产环境应该上报完整 sourceURL 吗?

通常不应直接上报带查询参数和片段的完整地址。先归一化到 origin 与 pathname,并限制字段、数量、采样率和保留时间。

参考资料:https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Long_animation_frame_timing;https://w3c.github.io/long-animation-frames/;https://web.dev/articles/find-slow-interactions-in-the-field。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>