长时间动画帧计时

长时间动画帧 (LoAFs) 会影响网站的用户体验。它们会导致用户界面 (UI) 更新缓慢,从而导致控件看似无响应,动画效果和滚动出现卡顿(或不流畅),进而导致用户感到沮丧。 长时间动画帧 API 允许开发人员获取有关长时间动画帧的信息,并更好地了解其根本原因。本文介绍了如何使用长时间动画帧 API。

什么是长时间动画帧?

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

良好的响应性意味着页面对交互的反应速度很快。这涉及及时绘制用户所需的所有更新,并避免任何可能阻止这些更新的操作。 例如,Google 的 交互到下一次绘制 (INP) 指标建议网站应该在 200 毫秒内响应页面交互(如点击或按键)。

为了实现流畅的动画,更新需要快速完成 - 为了使动画以每秒 60 帧的速度流畅运行,每个动画帧的渲染时间应该在 16 毫秒左右(1000/60)。

观察长时间动画帧

为了获取有关 LoAF 的信息并确定问题所在,可以使用标准的 PerformanceObserver 观察具有 "long-animation-frame"entryType 的性能时间轴条目。

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" 性能条目示例的 JSON 表示形式

json
{
  "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://web.dev/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 中所有 duration 大于 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.windowAttribution an script.window

一个枚举值,描述了执行此脚本的容器(即顶层文档或 <iframe>)与顶层文档的关系,以及对它的 Window 对象的引用。

注意:脚本属性仅提供给在页面主线程中运行的脚本,包括同源 <iframe>。但是,跨源 <iframe>Web 工作者服务工作者扩展 代码在长时间动画帧中将没有脚本属性,即使它们影响了其中一个动画帧的持续时间。

计算时间戳

PerformanceLongAnimationFrameTiming 类中提供的时间戳允许计算长时间动画帧的几个其他有用计时。

计时 计算
开始时间 startTime
结束时间 startTime + duration
工作时长 renderStart ? renderStart - startTime : duration
渲染时长 renderStart ? (startTime + duration) - renderStart: 0
渲染:预布局时长 styleAndLayoutStart ? styleAndLayoutStart - renderStart : 0
渲染:样式和布局时长 styleAndLayoutStart ? (startTime + duration) - styleAndLayoutStart : 0

示例

长时间动画帧 API 特性检测

可以使用 PerformanceObserver.supportedEntryTypes 测试长时间动画帧 API 是否受支持。

js
if (PerformanceObserver.supportedEntryTypes.includes("long-animation-frame")) {
  // Monitor LoAFs
}

报告超过特定阈值的 LoAF

虽然 LoAF 阈值固定为 50 毫秒,但在你开始性能优化工作时,这可能会导致大量报告。最初,你可能希望在更高的阈值下报告 LoAF,并在改进网站并消除最糟糕的 LoAF 后逐渐降低阈值。以下代码可用于捕获超过特定阈值的 LoAF 以供进一步分析(例如,通过将它们发送回分析端点)。

js
const REPORTING_THRESHOLD_MS = 150;

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > REPORTING_THRESHOLD_MS) {
      // Example here logs to console; real code could send to analytics endpoint
      console.log(entry);
    }
  }
});

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

长时间动画帧条目可能非常大;因此,请仔细考虑应该向分析发送每个条目中的哪些数据。例如,条目的摘要时间和脚本 URL 可能足以满足你的需求。

观察最长的动画帧

你可能希望只收集最长动画帧(例如前 5 或 10 个)的数据,以减少需要收集的数据量。这可以用以下方法处理

js
MAX_LOAFS_TO_CONSIDER = 10;
let longestBlockingLoAFs = [];

const observer = new PerformanceObserver((list) => {
  longestBlockingLoAFs = longestBlockingLoAFs
    .concat(list.getEntries())
    .sort((a, b) => b.blockingDuration - a.blockingDuration)
    .slice(0, MAX_LOAFS_TO_CONSIDER);
});
observer.observe({ type: "long-animation-frame", buffered: true });

// Report data on visibilitychange event
document.addEventListener("visibilitychange", () => {
  // Example here logs to console; real code could send to analytics endpoint
  console.log(longestBlockingLoAFs);
});

报告包含交互的长时间动画帧

另一个有用的技巧是在动画帧中发生交互的最大的 LoAF 条目中发送数据,这可以通过检查是否存在 firstUIEventTimestamp 值来检测。

以下代码记录所有大于 150 毫秒的 LoAF 条目,这些条目在动画帧期间发生了交互。你可以根据需要选择更高的或更低的值。

js
const REPORTING_THRESHOLD_MS = 150;

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (
      entry.duration > REPORTING_THRESHOLD_MS &&
      entry.firstUIEventTimestamp > 0
    ) {
      // Example here logs to console; real code could send to analytics endpoint
      console.log(entry);
    }
  }
});

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

识别长时间动画帧中的常见脚本模式

另一种策略是查看哪些脚本最常出现在 LoAF 条目中。可以在脚本级别和/或字符位置报告数据,以识别最有问题脚本。这在跨多个网站使用导致性能问题的主题或插件的情况下非常有用。

LoAF 中常见脚本(或第三方来源)的执行时间可以汇总并报告回来,以识别网站或网站集合中 LoAF 的常见贡献者。

例如,按 URL 对脚本进行分组并显示总持续时间

js
const observer = new PerformanceObserver((list) => {
  const allScripts = list.getEntries().flatMap((entry) => entry.scripts);
  const scriptSource = [
    ...new Set(allScripts.map((script) => script.sourceURL)),
  ];
  const scriptsBySource = scriptSource.map((sourceURL) => [
    sourceURL,
    allScripts.filter((script) => script.sourceURL === sourceURL),
  ]);
  const processedScripts = scriptsBySource.map(([sourceURL, scripts]) => ({
    sourceURL,
    count: scripts.length,
    totalDuration: scripts.reduce(
      (subtotal, script) => subtotal + script.duration,
      0,
    ),
  }));
  processedScripts.sort((a, b) => b.totalDuration - a.totalDuration);
  // Example here logs to console; real code could send to analytics endpoint
  console.table(processedScripts);
});

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

与长时间任务 API 比较

长动画帧 API 之前是 长任务 API(参见 PerformanceLongTaskTiming)。这两个 API 具有相似的目的和用途——公开有关 长任务 的信息,这些任务会阻塞主线程 50 毫秒或更长时间。

减少网站上出现的长任务数量非常有用,因为长任务会导致响应性问题。例如,如果用户在主线程处理长任务时单击按钮,则 UI 对单击的响应将被延迟,直到长任务完成。传统做法是将长任务分解成多个较小的任务,以便在任务之间处理重要的交互。

然而,长任务 API 存在局限性

  • 一个动画帧可以由多个任务组成,这些任务的持续时间低于 50 毫秒,但仍然会共同阻塞主线程。长动画帧 API 通过将动画帧作为一个整体来解决此问题。
  • PerformanceLongAnimationFrameTiming 类型相比,PerformanceLongTaskTiming 条目类型公开的信息更有限——它可以告诉您长任务发生的位置,但不能告诉您导致长任务的脚本或函数,例如。
  • 长任务 API 提供了一个不完整的视图,因为它可能排除了某些重要任务。某些更新(例如渲染)是在单独的任务中发生的,理想情况下应将其与导致该更新的先前执行一起包含,以便准确测量该交互的“总工作量”。

另请参阅