[译] Long Animation Frames 性能优化 API
原文链接:arc.net/l/quote/fpf…
原文作者:Barry Pollard, Noam Rosenthal
Long Animation Frames API(简称 LoAF,读作 Lo-Af)是对 Long Tasks API 的更新,旨在更好地理解慢用户界面(UI)更新。这对于识别可能影响Interaction to Next Paint(INP)核心 Web Vital 指标的响应性的慢动画帧非常有用,或者识别影响平滑度的其他 UI 卡顿现象。
API 的状态
在从 Chrome 116 到 Chrome 122 进行的一个起始试验之后,LoAF API 已经从 Chrome 123 开始推出。
背景:Long Tasks API
Long Animation Frames API 是 Long Tasks API 的一种替代方案,长期以来一直在 Chrome 中可用(自 Chrome 58 起)。顾名思义,Long Task API 允许您监控长任务,即占用主线程 50 毫秒或更长时间的任务。长任务可以使用 PerformanceLongTaskTiming
接口和 PeformanceObserver
进行监视:
const observer = new PerformanceObserver((list) => { console.log(list.getEntries());});
observer.observe({ type: 'longtask', buffered: true });
长任务可能会导致响应性问题。例如,如果用户尝试与页面进行交互,例如单击按钮或打开菜单,但主线程已经在处理长时间任务,则用户的交互会因等待任务完成而延迟。
为了改善响应性,通常建议分解长任务。如果每个长任务都被分解成多个较小的任务系列,可能会允许更重要的任务在它们之间执行,从而避免在响应交互时出现显着的延迟。
因此,在尝试改善响应性时,首先要进行的努力通常是运行性能跟踪并查看长任务。这可以通过类似 Lighthouse(具有 避免长时间主线程任务 审计)的基于实验室的审计工具,或者通过在 Chrome DevTools 中查看长任务来实现。
基于实验室的测试通常是识别响应性问题的较差起点,因为这些工具可能不包括交互 - 即使包括,它们也只是可能交互的一小部分。理想情况下,您应该在现场测量导致交互缓慢的原因。
Long Tasks API 的不足之处
在现场使用性能观察器(Performance Observer)测量长任务只能提供一些有限的信息。实际上,除了长任务发生的事实和持续时间外,并没有提供太多信息。
实际用户监控(RUM)工具通常使用此方法趋势化长任务的数量或持续时间,或者识别它们发生的页面,但是没有长任务的根本细节,这种方法的作用有限。Long Tasks API 只有一个基本的归因模型,最多只能告诉您发生长任务的容器(顶级文档或一个 <iframe>
),但不告诉您调用它的脚本或函数,如下所示:
{ "name": "unknown", "entryType": "longtask", "startTime": 31.799999997019768, "duration": 136, "attribution": [ { "name": "unknown", "entryType": "taskattribution", "startTime": 0, "duration": 0, "containerType": "window", "containerSrc": "", "containerId": "", "containerName": "" } ]}
Long Tasks API 还是一个不完整的视图,因为它可能排除了一些重要的任务。一些更新,如渲染,发生在单独的任务中,理想情况下应该与导致该更新的先前执行一起被包括,以准确测量该交互的“总工作量”。有关依赖任务的局限性的更多详细信息,请参阅说明中的“长任务的不足之处”部分。
最后一个问题是,仅测量长任务只报告超过 50 毫秒限制的单个任务。一个动画帧可能由几个小于此 50 毫秒限制的任务组成,但总体上仍会阻止浏览器的渲染能力。
Long Animation Frames API
长动画帧 API(LoAF)是一个新的 API,旨在解决长任务 API 的一些缺陷,使开发人员能够获得更可操作的见解,帮助解决响应性问题并改善 INP。
良好的响应性意味着页面能够快速响应用户的交互。这包括及时地绘制用户需要的任何更新,并避免阻止这些更新的发生。对于 INP,建议在 200 毫秒内做出响应,但对于其他更新(例如动画),甚至那可能也太长了。
长动画帧 API 是一种衡量阻塞工作的替代方法。与衡量单个 任务 不同,长动画帧 API——顾名思义——衡量 长动画帧。长动画帧是指渲染更新延迟超过 50 毫秒(与长任务 API 的阈值相同)的情况。
长动画帧可以通过类似于长任务的方式使用 PerformanceObserver
进行观察,但要查看的是 long-animation-frame
类型:
const observer = new PerformanceObserver((list) => {
console.log(list.getEntries());
});
observer.observe({ type: 'long-animation-frame', buffered: true });
以前的长动画帧也可以通过性能时间线进行查询:
const loafs = performance.getEntriesByType('long-animation-frame');
然而,性能条目存在maxBufferSize ,在达到该大小后,会丢弃较新的条目,因此,推荐使用PerformanceObserver方法。long-animation-frame
的缓冲区大小设置为200,与long-tasks
相同。
以帧为单位而不是任务的优势
以帧为单位而不是任务的视角的主要优势在于,长动画可能由任意数量的任务累积导致长动画帧。这解决了先前提到的最后一点,即动画帧之前许多较小的阻塞渲染任务的总和可能不会被Long Tasks API显示出来的问题。
这种对长任务的另类视角的另一个优势在于,能够提供整个帧的定时细分。与Long Tasks API只包含startTime
和duration
不同,LoAF包含了对帧持续时间的各个部分的更详细的分解,包括:
startTime
:相对于导航开始时间的长动画帧的开始时间。duration
:长动画帧的持续时间(不包括呈现时间)。renderStart
:渲染循环开始的时间,包括requestAnimationFrame
回调、样式和布局计算、调整大小观察器和交集观察器回调。styleAndLayoutStart
:在样式和布局计算中花费的时间段的开始时间。firstUIEventTimestamp
:在此帧过程中处理的第一个UI事件(鼠标/键盘等)的时间。blockingDuration
:动画帧被阻塞的持续时间,以毫秒为单位。
这些时间戳允许将长动画帧分解为以下定时:
定时 | 计算 |
---|---|
开始时间 | startTime |
结束时间 | startTime + duration |
工作持续时间 | renderStart ? renderStart - startTime : duration |
渲染持续时间 | renderStart ? (startTime + duration) - renderStart: 0 |
渲染:预布局持续时间 | styleAndLayoutStart ? styleAndLayoutStart - renderStart : 0 |
渲染:样式和布局持续时间 | styleAndLayoutStart ? (startTime + duration) - styleAndLayoutStart : 0 |
关于这些单独定时的更多细节,请参阅说明文档,其中详细说明了哪些活动导致了长动画帧。
更好的归因
long-animation-frame
条目类型包含每个贡献到长动画帧的脚本的更好的归因数据。
与Long Tasks API类似,这将以归因条目数组的形式提供,其中每个条目都详细说明了:
name
和EntryType
都将返回script
。- 有意义的
invoker
,指示脚本的调用方式(例如,'IMG#id.onload'
,'Window.requestAnimationFrame'
或'Response.json.then'
)。 - 脚本条目点的
invokerType
:user-callback
:从Web平台API注册的已知回调(例如setTimeout
,requestAnimationFrame
)。event-listener
:平台事件的监听器(例如click
,load
,keyup
)。resolve-promise
:平台Promise的处理程序(例如fetch()
。请注意,在Promise的情况下,同一个Promise的所有处理程序都被混合在一起,作为一个 "script")。reject-promise
:与resolve-promise
相同,但用于拒绝。classic-script
:脚本评估(例如,<script>
或import()
)module-script
:与classic-script
相同,但用于模块脚本。
- 该脚本的单独计时数据:
startTime
:调用条目函数的时间。duration
:startTime
和后续微任务队列完成处理的时间之间的持续时间。executionStart
:编译后的时间。forcedStyleAndLayoutDuration
:在此函数内处理强制布局和样式的总时间(请参阅thrashing)。pauseDuration
:在 "暂停" 同步操作(alert,同步XHR)中花费的总时间。
- 脚本源细节:
sourceURL
:脚本资源名称(如果可用)(如果未找到,则为空)。sourceFunctionName
:脚本函数名称(如果可用)(如果未找到,则为空)。sourceCharPosition
:脚本字符位置(如果可用)(如果未找到,则为 -1)。
windowAttribution
:发生长动画帧的容器(顶级文档或<iframe>
)。window
:同源窗口的引用。
提供源条目时,允许开发人员准确了解长动画帧中的每个脚本是如何调用的,直到调用脚本的字符位置。这提供了导致长动画帧的JavaScript资源中的确切位置。
long-animation-frame
性能条目示例
一个完整的 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://web.dev/js/index-ffde4443.js",
"sourceFunctionName": "myClickHandler",
"sourceCharPosition": 17796,
"startTime": 11803.199999999255,
"window": [Window object],
"windowAttribution": "self"
}
],
"startTime": 11802.400000000373,
"styleAndLayoutStart": 11858.800000000745
}
正如所见,这为网站提供了前所未有的数据量,使其能够理解导致渲染更新延迟的原因。
启用长动画帧 API
长动画帧 API 在 Chrome 123 中默认启用。
在现场使用长动画帧 API
像 Lighthouse 这样的工具虽然有助于发现和重现问题,但它们是实验室工具,可能会忽略只有现场数据才能提供的用户体验的重要方面。长动画帧 API 可以在现场使用,收集用户交互的重要上下文数据,而长任务 API 无法提供这些数据。这可以帮助您识别和重现您可能尚未发现的交互问题。
接下来列出了一些建议的策略,但 Chrome 团队渴望听取有关此 API 的反馈,以及开发人员和 RUM 提供商将如何使用 API 提供的信息。
功能检测长动画帧 API 支持
您可以使用以下代码测试 API 是否受支持:
if (PerformanceObserver.supportedEntryTypes.includes('long-animation-frame')) {
// Monitor LoAFs
}
将长动画数据报告回分析端点
如所示,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, but could also report back to analytics
console.log(entry);
}
}
});
observer.observe({ type: 'long-animation-frame', buffered: true });
长动画帧条目可能相当庞大,开发人员应决定从条目中发送哪些数据到分析系统。例如,条目的摘要时间以及可能的脚本名称,或者其他一些被视为必要的最小上下文数据。
观察最严重的长动画帧
网站可能希望收集关于最长动画帧(或帧)的数据,以减少需要回传的数据量。因此,无论页面经历了多少个长动画帧,只有最严重的一个、五个或绝对必要的长动画帧的数据会被回传。
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 });
在适当的时候(最好是在 visibilitychange
事件上)将数据回传给分析端点。对于本地测试,可以定期使用 console.table
:
console.table(longestBlockingLoAFs);
链接到最长的 INP 交互
作为观察最严重 LoAF 的延伸,可以使用与 INP 条目相对应的 LoAF 帧作为归因数据,以提供如何改进 INP 的更多详细信息。
目前没有直接的 API 可以将 INP 条目与其相关的 LoAF 条目或条目进行关联,但是可以通过比较每个条目的开始和结束时间来在代码中执行此操作(参见 WhyNp 示例脚本)。
web-vitals
库 从 v4 开始在 INP 归因接口的 longAnimationFramesEntries
属性中包含所有相交的 LoAF。
报告带有交互的长动画帧
另一种更简单的方法是始终发送在帧期间发生交互的最大(或前 X 最大)LoAF 条目,其中可以通过帧期间存在 firstUIEventTimestamp
值来检测到交互。在大多数情况下,这将包括给定访问的 INP 交互,而在罕见情况下,当它不包含时,仍会显示重要的长交互以修复,因为它们可能是其他用户的 INP 交互。
以下代码记录了在帧期间发生交互且持续时间超过 150 毫秒的所有 LoAF 条目。这里选择了 150 是因为它略小于 200 毫秒的“良好”INP 阈值。您可以根据需要选择更高或更低的值。
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, but could also report back to analytics
console.log(entry);
}
}
});
observer.observe({ type: 'long-animation-frame', buffered: true });
识别长动画帧中的常见模式
另一种策略是查看长动画帧条目中最常出现的常见脚本。可以在脚本和字符位置级别报告数据,以识别重复出现的问题。
这在可定制平台中特别有效,在这些平台中,可以跨多个站点识别出导致性能问题的主题或插件。
可以将长动画帧中常见脚本或第三方来源的执行时间汇总并报告,识别出跨站点或一组站点的长动画帧中的常见贡献者。例如,可以查看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, but could also report back to analytics
console.table(processedScripts);
});
observer.observe({type: 'long-animation-frame', buffered: true});
一个示例输出是:
(index) | sourceURL | count | totalDuration |
---|---|---|---|
0 | 'https://example.consent.com/consent.js' | 1 | 840 |
1 | 'https://example.com/js/analytics.js' | 7 | 628 |
2 | 'https://example.chatapp.com/web-chat.js' | 1 | 5 |
该API还可以允许额外的开发者工具进行本地调试。虽然像Lighthouse和Chrome DevTools这样的工具已经能够使用低级别的跟踪详细信息收集许多数据,但拥有这个高级API可以允许其他工具访问这些数据。
在DevTools中展示长动画帧数据
你可以使用performance.measure()
API在DevTools中展示长动画帧数据,这些数据随后会显示在DevTools用户时间跟踪的性能跟踪中,以显示需要集中精力进行性能改进的地方:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
performance.measure('LoAF', {
start: entry.startTime,
end: entry.startTime + entry.duration,
});
}
});
observer.observe({ type: 'long-animation-frame', buffered: true });
如果这个API在长期内证明有用,它可能会被纳入DevTools本身,但前面的代码片段允许它在此期间显示在那里。
在其他开发者工具中使用长动画帧数据
Web Vitals扩展展示了记录摘要调试信息以诊断性能问题的价值。现在API已经发布,像这样的工具可以显示数据,以帮助开发者了解应该集中精力的地方。我们还计划在web vitals JavaScript库的第4版中添加这一功能。
在自动化测试工具中使用长动画帧数据
同样,自动化测试工具(例如在CI/CD流水线中)可以通过在运行各种测试套件时测量长动画帧,来显示潜在的性能问题的详细信息。
FAQ
关于这个API的一些常见问题包括:
为什么不直接扩展或迭代长任务API?
这是另一种报告潜在响应问题的类似但最终不同的度量方法。确保依赖现有长任务API的网站继续运行以避免中断现有使用案例是很重要的。
虽然长任务API可能从LoAF的一些特性中受益(例如更好的归因模型),但我们认为,专注于帧而不是任务带来了许多好处,这使得这个API与现有的长任务API在本质上有所不同。
这个API会取代长任务API吗?
虽然我们认为长动画帧API是测量长任务的一个更好、更完整的API,但目前没有计划废弃长任务API。
反馈
可以在GitHub问题列表提供反馈,或在Chrome问题跟踪器中提交关于Chrome实现该API的错误报告。
结论
长动画帧API是一个令人兴奋的新API,相比之前的长任务API有许多潜在的优势。
它被证明是解决INP测量的响应问题的关键工具。INP是一个优化难度较大的指标,而这个API是Chrome团队希望通过它来简化开发者识别和解决问题的一种方式。
长动画帧API的范围不仅限于INP,它还可以帮助识别其他导致更新缓慢的问题,这些问题会影响网站用户体验的整体流畅度。
转载自:https://juejin.cn/post/7372135071971459087