长动画帧计时
长动画帧(Long Animation Frames,LoAFs)会影响网站的用户体验。它们可能导致用户界面(UI)更新缓慢,从而使得控件看似无响应,动画效果和滚动出现卡顿(或不流畅),进而引起用户沮丧。长动画帧 API 允许开发者获取关于长动画帧的信息,从而更好地理解其根本原因。本文将展示如何使用长动画帧 API。
什么是长动画帧?
长动画帧 (LoAF) 是指渲染更新延迟超过 50 毫秒的情况。
良好的响应能力意味着页面能够快速响应交互。这包括及时绘制用户所需的任何更新,并避免任何可能阻碍这些更新的情况。例如,Google 的交互到下一帧渲染 (INP) 指标建议网站应在 200 毫秒内响应页面交互(如点击或按键)。
为了使动画流畅,更新必须快速——为了使动画以每秒 60 帧流畅运行,每个动画帧应在大约 16 毫秒内渲染(1000/60)。
观察长动画帧
要获取 LoAF 的信息并找出问题所在,您可以使用标准的 PerformanceObserver 观察 entryType 为 "long-animation-frame" 的性能时间线条目。
const observer = new PerformanceObserver((list) => {
console.log(list.getEntries());
});
observer.observe({ type: "long-animation-frame", buffered: true });
也可以使用 Performance.getEntriesByType() 等方法查询先前的长动画帧。
const loafs = performance.getEntriesByType("long-animation-frame");
但是请注意,"long-animation-frame" 条目类型的最大缓冲区大小为 200,超过此限制后新条目将被丢弃,因此建议使用 PerformanceObserver 方法。
检查 "long-animation-frame" 条目
类型为 "long-animation-frame" 的性能时间线条目由 PerformanceLongAnimationFrameTiming 对象表示。此对象具有一个 scripts 属性,其中包含一个 PerformanceScriptTiming 对象的数组,每个对象都包含有关导致长动画帧的脚本的信息。
以下是一个完整的 "long-animation-frame" 性能条目示例,其中包含单个脚本。
({
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.invoker和script.invokerType-
字符串值,表示脚本是如何调用的(例如,
"IMG#id.onload"或"Window.requestAnimationFrame"),以及脚本入口点类型(例如,"event-listener"或"resolve-promise")。 script.pauseDuration-
一个
DOMHighResTimeStamp,表示脚本在“暂停”同步操作(例如,Window.alert()调用或同步XMLHttpRequest)上花费的总毫秒时间。 script.sourceCharPosition、script.sourceFunctionName和script.sourceURL-
分别表示脚本字符位置、函数名和脚本 URL 的值。需要注意的是,报告的函数名将是脚本的“入口点”(即堆栈的顶层),而不是任何特定的慢速子函数。
例如,如果事件处理程序调用一个顶层函数,而该顶层函数又调用一个慢速子函数,则
source*字段将报告顶层函数的名称和位置,而不是慢速子函数。这是出于性能原因——完整的堆栈跟踪开销很大。 script.windowAttribution和script.window-
一个枚举值,描述了该脚本执行所在的容器(即顶层文档或
)与顶层文档之间的关系,以及对其Window对象的引用。
注意:脚本归因仅提供给在页面主线程中运行的脚本,包括同源的
。然而,跨域的、Web Workers、Service Workers 和扩展代码在长动画帧中不会有脚本归因,即使它们影响了长动画帧的持续时间。
计算时间戳
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。
if (PerformanceObserver.supportedEntryTypes.includes("long-animation-frame")) {
// Monitor LoAFs
}
报告超过特定阈值的 LoAF
虽然 LoAF 的阈值固定为 50 毫秒,但在您首次开始性能优化工作时,这可能会导致大量报告。最初,您可能希望报告较高阈值值的 LoAF,并随着您改进网站和消除最严重的 LoAF 而逐渐降低阈值。以下代码可用于捕获超过特定阈值的 LoAF 以进行进一步分析(例如,通过将它们发送回分析端点)。
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 个),以减少需要收集的数据量。这可以按如下方式处理:
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 条目。您可以根据需要选择更高或更低的值。
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 分组脚本并显示总持续时间:
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 也有其局限性:
- 一个动画帧可能由几个低于 50ms 阈值的任务组成,但它们仍然集体阻塞主线程。长动画帧 API 通过将动画帧视为一个整体来解决这个问题。
PerformanceLongTaskTiming条目类型比PerformanceLongAnimationFrameTiming类型暴露的信息更有限——例如,它可以告诉您长任务发生的容器,但不能告诉您导致它的脚本或函数。- 长任务 API 提供了一个不完整的视图,因为它可能排除一些重要的任务。一些更新(例如渲染)发生在单独的任务中,理想情况下应该与导致该更新的先行执行一起包含,以准确测量该交互的“总工作量”。