长时间动画帧计时
长时间动画帧 (LoAFs) 会影响网站的用户体验。它们会导致用户界面 (UI) 更新缓慢,从而导致控件看似无响应,动画效果和滚动出现卡顿(或不流畅),进而导致用户感到沮丧。 长时间动画帧 API 允许开发人员获取有关长时间动画帧的信息,并更好地了解其根本原因。本文介绍了如何使用长时间动画帧 API。
什么是长时间动画帧?
长时间动画帧(或 LoAF)是指渲染更新延迟超过 50 毫秒的情况。
良好的响应性意味着页面对交互的反应速度很快。这涉及及时绘制用户所需的所有更新,并避免任何可能阻止这些更新的操作。 例如,Google 的 交互到下一次绘制 (INP) 指标建议网站应该在 200 毫秒内响应页面交互(如点击或按键)。
为了实现流畅的动画,更新需要快速完成 - 为了使动画以每秒 60 帧的速度流畅运行,每个动画帧的渲染时间应该在 16 毫秒左右(1000/60)。
观察长时间动画帧
为了获取有关 LoAF 的信息并确定问题所在,可以使用标准的 PerformanceObserver
观察具有 "long-animation-frame"
的 entryType
的性能时间轴条目。
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"
性能条目示例的 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.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
anscript.window
-
一个枚举值,描述了执行此脚本的容器(即顶层文档或
<iframe>
)与顶层文档的关系,以及对它的Window
对象的引用。
计算时间戳
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 存在局限性
- 一个动画帧可以由多个任务组成,这些任务的持续时间低于 50 毫秒,但仍然会共同阻塞主线程。长动画帧 API 通过将动画帧作为一个整体来解决此问题。
- 与
PerformanceLongAnimationFrameTiming
类型相比,PerformanceLongTaskTiming
条目类型公开的信息更有限——它可以告诉您长任务发生的位置,但不能告诉您导致长任务的脚本或函数,例如。 - 长任务 API 提供了一个不完整的视图,因为它可能排除了某些重要任务。某些更新(例如渲染)是在单独的任务中发生的,理想情况下应将其与导致该更新的先前执行一起包含,以便准确测量该交互的“总工作量”。