长动画帧计时

长动画帧(Long Animation Frames,LoAFs)会影响网站的用户体验。它们可能导致用户界面(UI)更新缓慢,从而使得控件看似无响应,动画效果和滚动出现卡顿(或不流畅),进而引起用户沮丧。长动画帧 API 允许开发者获取关于长动画帧的信息,从而更好地理解其根本原因。本文将展示如何使用长动画帧 API。

什么是长动画帧?

长动画帧 (LoAF) 是指渲染更新延迟超过 50 毫秒的情况。

良好的响应能力意味着页面能够快速响应交互。这包括及时绘制用户所需的任何更新,并避免任何可能阻碍这些更新的情况。例如,Google 的交互到下一帧渲染 (INP) 指标建议网站应在 200 毫秒内响应页面交互(如点击或按键)。

为了使动画流畅,更新必须快速——为了使动画以每秒 60 帧流畅运行,每个动画帧应在大约 16 毫秒内渲染(1000/60)。

观察长动画帧

要获取 LoAF 的信息并找出问题所在,您可以使用标准的 PerformanceObserver 观察 entryType"long-animation-frame" 的性能时间线条目。

js
const observer = new PerformanceObserver((list) => {
  console.log(list.getEntries());
});

observer.observe({ type: "long-animation-frame", buffered: true });

也可以使用 Performance.getEntriesByType() 等方法查询先前的长动画帧。

js
const loafs = performance.getEntriesByType("long-animation-frame");

但是请注意,"long-animation-frame" 条目类型的最大缓冲区大小为 200,超过此限制后新条目将被丢弃,因此建议使用 PerformanceObserver 方法。

检查 "long-animation-frame" 条目

类型为 "long-animation-frame" 的性能时间线条目由 PerformanceLongAnimationFrameTiming 对象表示。此对象具有一个 scripts 属性,其中包含一个 PerformanceScriptTiming 对象的数组,每个对象都包含有关导致长动画帧的脚本的信息。

以下是一个完整的 "long-animation-frame" 性能条目示例,其中包含单个脚本。

js
({
  blockingDuration: 0,
  duration: 60,
  entryType: "long-animation-frame",
  firstUIEventTimestamp: 11801.099999999627,
  name: "long-animation-frame",
  renderStart: 11858.800000000745,
  scripts: [
    {
      duration: 45,
      entryType: "script",
      executionStart: 11803.199999999255,
      forcedStyleAndLayoutDuration: 0,
      invoker: "DOMWindow.onclick",
      invokerType: "event-listener",
      name: "script",
      pauseDuration: 0,
      sourceURL: "https://webdev.ac.cn/js/index-ffde4443.js",
      sourceFunctionName: "myClickHandler",
      sourceCharPosition: 17796,
      startTime: 11803.199999999255,
      window: {
        // …Window object…
      },
      windowAttribution: "self",
    },
  ],
  startTime: 11802.400000000373,
  styleAndLayoutStart: 11858.800000000745,
});

除了 PerformanceEntry 条目返回的标准数据外,它还包含以下值得注意的项:

blockingDuration

一个 DOMHighResTimeStamp,表示主线程被阻塞,无法响应高优先级任务(例如用户输入)的总毫秒时间。它的计算方式是:获取 LoAF 中所有持续时间超过 50ms长任务,从每个任务中减去 50ms,将渲染时间加到最长任务时间上,然后将结果求和。

firstUIEventTimestamp

一个 DOMHighResTimeStamp,表示在当前动画帧期间处理的第一个 UI 事件(例如鼠标或键盘事件)的时间。请注意,如果事件发生和处理之间存在延迟,此时间戳可能在动画帧开始之前。

renderStart

一个 DOMHighResTimeStamp,表示渲染周期的开始时间,包括 Window.requestAnimationFrame() 回调、样式和布局计算、ResizeObserver 回调以及 IntersectionObserver 回调。

styleAndLayoutStart

一个 DOMHighResTimeStamp,表示当前动画帧的样式和布局计算开始的时间段。

PerformanceScriptTiming 属性

提供有关导致 LoAF 的脚本信息的属性。

script.executionStart

一个 DOMHighResTimeStamp,表示脚本编译完成并开始执行的时间。

script.forcedStyleAndLayoutDuration

一个 DOMHighResTimeStamp,表示脚本处理强制布局/样式所花费的总毫秒时间。请参阅避免布局抖动以了解其原因。

script.invokerscript.invokerType

字符串值,表示脚本是如何调用的(例如,"IMG#id.onload""Window.requestAnimationFrame"),以及脚本入口点类型(例如,"event-listener""resolve-promise")。

script.pauseDuration

一个 DOMHighResTimeStamp,表示脚本在“暂停”同步操作(例如,Window.alert() 调用或同步 XMLHttpRequest)上花费的总毫秒时间。

script.sourceCharPositionscript.sourceFunctionNamescript.sourceURL

分别表示脚本字符位置、函数名和脚本 URL 的值。需要注意的是,报告的函数名将是脚本的“入口点”(即堆栈的顶层),而不是任何特定的慢速子函数。

例如,如果事件处理程序调用一个顶层函数,而该顶层函数又调用一个慢速子函数,则 source* 字段将报告顶层函数的名称和位置,而不是慢速子函数。这是出于性能原因——完整的堆栈跟踪开销很大。

script.windowAttributionscript.window

一个枚举值,描述了该脚本执行所在的容器(即顶层文档或