了解如何衡量动画、如何考虑动画帧以及整体页面平滑度。
您可能遇到过在滚动或动画播放过程中网页“卡顿”或“冻结”的情况。我们通常会说这些体验不够顺畅。为了解决这些类型的问题,Chrome 团队一直在努力为动画检测添加更多对实验室工具的支持,并不断改进 Chromium 中的渲染流水线诊断功能。
我们想分享一些近期的进展,提供具体的工具使用指南,并讨论未来动画流畅度指标的相关想法。我们一如既往地期待收到您的反馈。
这篇博文将涵盖三个主要主题:
- 动画和动画帧速览。
- 我们目前在衡量整体动画流畅度方面的想法。
- 以下是一些实用建议,可帮助您立即利用实验室工具。
什么是动画?
动画让内容栩栩如生!通过让内容移动(尤其是响应用户互动),动画可以使体验更自然、更易于理解且更有趣。
但如果动画实现不当,或者只是添加了过多的动画,则会降低体验,让用户感觉一点也不好玩。我们可能都曾与添加了过多“有帮助”的过渡效果的界面互动过,但当这些效果表现不佳时,实际上会损害用户体验。 因此,有些用户实际上可能偏好减少动态效果,您应尊重这一用户偏好。
动画是如何运作的?
简单来说,渲染流水线由几个按顺序执行的阶段组成:
- 样式:计算应用于元素的样式。
- 布局:为每个元素生成几何形状和位置。
- 绘制:将每个元素的像素填充到图层中。
- 合成:将图层绘制到屏幕上。
虽然定义动画的方法有很多,但它们从根本上来说都是通过以下方式之一实现的:
- 调整布局属性。
- 调整绘制属性。
- 调整复合属性。
由于这些阶段是按顺序进行的,因此务必根据流水线中更下游的属性来定义动画。在流程中越早进行更新,成本就越高,顺利完成的可能性就越低。(如需了解详情,请参阅渲染性能)。
虽然为布局属性添加动画效果很方便,但这样做会产生一些成本,即使这些成本并不立即可见。应尽可能根据复合属性变化来定义动画。
定义声明性 CSS 动画或使用 Web Animations,并确保您以动画方式呈现复合属性,是确保动画流畅高效的第一步。但即便如此,这本身并不能保证流畅性,因为即使是高效的 Web 动画也有性能限制。因此,效果衡量始终至关重要!
什么是动画帧?
页面视觉效果的更新需要一段时间才能显示。视觉变化会导致生成新的动画帧,最终在用户显示屏上呈现。
以一定间隔显示更新,因此视觉更新会分批进行。许多显示屏会以固定的时间间隔进行更新,例如每秒 60 次(即 60 Hz)。一些较新的显示屏可以提供更高的刷新频率(90-120 Hz 正在变得越来越常见)。通常,这些显示屏可以根据需要主动在刷新率之间进行调整,甚至提供完全可变的帧速率。
任何应用(例如游戏或浏览器)的目标都是处理所有这些批处理的视觉更新,并在每次截止期限内生成视觉上完整的动画帧。请注意,此目标与从网络快速加载内容或高效执行 JavaScript 任务等其他重要的浏览器任务完全不同。
在某个时间点,可能很难在显示屏分配的截止期限内完成所有视觉更新。发生这种情况时,浏览器会丢弃帧。屏幕不会变黑,只是不断重复显示。您会看到相同的视觉更新持续更长时间,即在之前的帧机会中呈现的同一动画帧。
这种情况其实经常发生!这种变化甚至可能难以察觉,尤其是对于静态或类似文档的内容(在 Web 平台上尤其常见)。只有在出现重要的视觉更新(例如动画)时,丢帧才会变得明显,因为我们需要稳定的动画更新流才能显示流畅的动作。
哪些因素会影响动画帧?
Web 开发者可以极大地影响浏览器快速高效地渲染和呈现视觉更新的能力!
一些示例:
- 使用过大或资源消耗过多的内容,导致无法在目标设备上快速解码。
- 使用过多的层,需要过多的 GPU 内存。
- 定义过于复杂的 CSS 样式或网页动画。
- 使用会停用快速渲染优化的设计反模式。
- 主线程上的 JS 工作过多,导致长任务阻塞视觉更新。
但如何知道动画帧何时错过了截止时间并导致丢帧?
一种可能的方法是使用 requestAnimationFrame()
轮询,但这种方法存在一些缺点。requestAnimationFrame()
(或“rAF”)会告知浏览器您希望执行动画,并请求在渲染流水线的下一个绘制阶段之前获得执行动画的机会。如果您的回调函数未在预期时间被调用,则表示未执行绘制,并且跳过了一个或多个帧。通过轮询并统计 rAF 的调用频率,您可以计算出一种“每秒帧数”(FPS) 指标。
let frameTimes = [];
function pollFramesPerSecond(now) {
frameTimes = [...frameTimes.filter(t => t > now - 1000), now];
requestAnimationFrame(pollFramesPerSecond);
console.log('Frames per second:', frameTimes.length);
}
requestAnimationFrame(pollFramesPerSecond);
使用 requestAnimationFrame()
轮询不是一个好主意,原因如下:
- 每个脚本都必须设置自己的轮询循环。
- 它可能会阻塞关键路径。
- 即使 rAF 轮询速度很快,如果连续使用,它也会阻止
requestIdleCallback()
安排较长的空闲块(超过单个帧的块)。 - 同样,缺少长时间的空闲块会阻止浏览器调度其他长时间运行的任务(例如更长时间的垃圾回收和其他后台或推测性工作)。
- 如果轮询功能处于开启和关闭状态,您将错过帧预算超出时的情形。
- 如果浏览器使用可变更新频率(例如,由于电池电量或可见性状态),轮询会报告假正例。
- 最重要的是,它实际上并未捕获所有类型的动画更新!
主线程上的工作过多可能会影响动画帧的显示。 您可以查看 Jank Sample,了解在主线程上工作过多(例如布局)时,由 rAF 驱动的动画将如何导致丢帧、rAF 回调次数减少和 FPS 降低。
当主线程变得拥塞时,视觉更新会开始卡顿。这就是卡顿!
许多衡量工具都非常注重主线程及时让步的能力,以及动画帧能否流畅运行。但这并不是全部事实!请参考以下示例:
上面的视频展示了一个定期将长时间运行的任务注入到主线程的网页。这些长时间运行的任务会完全破坏网页提供某些类型视觉更新的能力,您可以在左上角看到报告的 FPS 相应下降到 0。requestAnimationFrame()
尽管任务耗时较长,但网页仍能流畅滚动。这是因为在现代浏览器中,滚动通常是线程化的,完全由合成器驱动。
此示例同时包含主线程上的许多丢弃帧,但仍有许多在合成器线程上成功传送的滚动帧。长任务完成后,主线程绘制更新无论如何都不会提供视觉变化。rAF 轮询表明帧丢弃为 0,但在视觉上,用户无法注意到差异!
对于动画帧,情况就没那么简单了。
动画帧:重要更新
上述示例表明,除了 requestAnimationFrame()
之外,还有更多内容。
那么,动画更新和动画帧何时重要?以下是我们正在考虑的一些标准,欢迎您提供反馈:
- 主线程和合成器线程更新
- 缺少涂装更新
- 检测动画
- 质量与数量
主线程和合成器线程更新
动画帧更新不是布尔值。帧并非只能完全丢弃或完全呈现。动画帧可能部分 呈现的原因有很多。换句话说,它可能同时包含一些过时的内容,同时也包含一些新的视觉更新。
最常见的示例是,浏览器无法在帧截止期限内生成新的主线程更新,但确实有新的合成器线程更新(例如之前的线程化滚动示例)。
建议使用声明式动画来为复合属性设置动画的一个重要原因是,即使主线程处于繁忙状态,这样做也能使动画完全由合成器线程驱动。这些类型的动画可以继续高效地并行生成视觉更新。
另一方面,在某些情况下,主线程更新最终可用于呈现,但前提是错过了多个帧截止时间。此时,浏览器将有一些新更新,但可能不是最新的。
从广义上讲,我们认为包含部分新视觉更新(而非全部新视觉更新)的帧是部分帧。部分帧相当常见。理想情况下,部分更新至少应包含最重要的视觉更新(例如动画),但只有在动画由合成器线程驱动时才能实现这一点。
缺少涂装更新
另一种类型的局部更新是,当图片等媒体在帧呈现之前未及时完成解码和栅格化时。
或者,即使网页完全静态,浏览器在快速滚动时仍可能无法及时呈现视觉更新。这是因为超出可见视口的内容的像素渲染可能会被舍弃,以节省 GPU 内存。渲染像素需要时间,并且在大幅滚动(例如手指快速滑动)后渲染所有内容可能需要的时间超过单个帧。这通常称为棋盘格。
在每次帧渲染机会中,都可以跟踪有多少最新的视觉更新实际到达了屏幕。在多个帧(或时间)内衡量这种能力通常称为帧吞吐量。
如果 GPU 真的不堪重负,浏览器(或平台)甚至可能会开始限制其尝试进行视觉更新的速率,从而降低有效帧速率。虽然从技术上讲,这可以减少丢弃的帧更新数量,但从视觉上讲,帧吞吐量仍会显得较低。
不过,并非所有类型的低帧吞吐量都是坏事。如果网页大部分时间处于闲置状态,并且没有正在运行的动画,那么低帧速率在视觉效果上与高帧速率一样出色(而且还可以节省电池电量!)。
那么,帧吞吐量何时重要?
检测动画
高帧吞吐量在重要动画播放期间尤为重要。不同的动画类型将取决于特定线程(主线程、合成器或工作器)的视觉更新,因此其视觉更新取决于该线程是否在截止期限内提供更新。如果某个线程更新会影响正在运行的动画,我们就说该线程影响流畅度。
与某些类型的动画相比,另一些类型的动画更容易定义和检测。 声明式动画或用户输入驱动的动画比 JavaScript 驱动的动画(实现为可动画样式的定期更新)更易于定义。
即使使用 requestAnimationFrame()
,您也无法始终假设每次 rAF 调用都一定会产生视觉更新或动画。例如,仅使用 rAF 轮询来跟踪帧速率(如上所示)本身不应影响平滑度测量,因为没有视觉更新。
质量与数量
最后,检测动画和动画帧更新仍然只是故事的一部分,因为它只捕获动画更新的数量,而不是质量。
例如,您在观看视频时可能会看到稳定的 60 fps 帧速率。从技术上讲,这完全流畅,但视频本身可能比特率较低,或者存在网络缓冲问题。动画流畅度指标不会直接捕获此问题,但用户仍可能会感到不流畅。
或者,利用 <canvas>
的游戏(甚至可能使用屏幕外画布等技术来确保稳定的帧速率)在动画帧方面可能在技术上非常流畅,但同时无法将高质量的游戏资源加载到场景中或显示渲染伪影。
当然,网站也可能只是包含一些非常糟糕的动画 🙂
我的意思是,我猜它们在当时应该很酷!
单个动画帧的状态
由于帧可能会部分呈现,或者丢帧可能会以不影响平滑度的方式发生,因此我们开始将每个帧视为具有完整性或平滑度得分。
下面列出了我们解读单个动画帧状态的各种方式,从最佳情况到最差情况依次排列:
不需要更新 | 空闲时间,重复上一帧。 |
完全呈现 | 主线程更新已在截止期限内提交,或者不需要主线程更新。 |
部分呈现 | 仅限合成器;延迟的主线程更新没有视觉变化。 |
部分呈现 | 仅限合成器;主线程有视觉更新,但该更新不包含影响平滑度的动画。 |
部分呈现 | 仅限合成器;主线程有影响平滑度的视觉更新,但之前过时的帧到达并被使用。 |
部分呈现 | 仅限合成器;没有所需的主要更新,并且合成器更新具有影响平滑度的动画。 |
部分呈现 | 仅限合成器,但合成器更新不包含影响平滑度的动画。 |
丢弃的帧 | 无更新。不需要合成器更新,但主版本延迟了。 |
丢弃的帧 | 需要进行合成器更新,但该更新被延迟了。 |
过时帧 | 需要更新,渲染器生成了更新,但 GPU 仍然未在垂直同步截止时间之前呈现更新。 |
您可以将这些状态转换为某种程度的分数。或许可以将此得分解读为用户可观测到的概率。单个丢帧可能不太明显,但连续丢弃许多帧肯定会影响流畅度!
综合应用:丢帧百分比指标
虽然有时需要深入了解每个动画帧的状态,但快速为体验分配一个“一目了然”的分数也很有用。
由于帧可能部分呈现,并且即使完全跳过的帧更新也可能不会实际影响流畅度,因此我们希望减少对仅计算帧的关注,而更多地关注浏览器在重要时刻无法提供视觉上完整更新的程度。
心理模型应从以下方面入手:
- 每秒帧数,以
- 检测缺失的重要动画更新,以
- 指定时间段内的降幅百分比。
重要的是:等待重要更新所占的时间比例。我们认为,这与用户在实践中体验网页内容流畅度的自然方式相符。到目前为止,我们一直使用以下指标作为初始指标集:
- 平均丢弃百分比:整个时间轴中所有非空闲动画帧的平均丢弃百分比
- 丢帧百分比最差情况:以 1 秒滑动时间窗口为单位进行测量。
- 丢帧百分比的第 95 百分位:以 1 秒的滑动时间窗口为单位进行测量。
您现在可以在某些 Chrome 开发者工具中找到这些得分。虽然这些指标仅侧重于总体帧吞吐量,但我们也在评估其他因素,例如帧延迟。
在开发者工具中亲自试一试吧!
性能 HUD
Chromium 有一个隐藏在标志 (chrome://flags/#show-performance-metrics-hud
) 后面的实用性能 HUD。在其中,您可以找到核心网页指标等内容的实时得分,还可以找到一些基于一段时间内的丢帧百分比的动画流畅度实验性定义。
帧渲染统计信息
通过“渲染”设置在开发者工具中启用“帧渲染统计信息”,即可实时查看新的动画帧,这些动画帧会通过颜色编码来区分部分更新和完全丢弃的帧更新。报告的 FPS 仅针对完全呈现的帧。
开发者工具性能配置文件记录中的帧查看器
开发者工具的“性能”面板长期以来一直具有帧查看器。 不过,它与现代渲染流水线的实际运作方式有些不合拍。最近,我们进行了大量改进,即使在最新的 Chrome Canary 中,我们也添加了许多功能,相信这些功能将大大简化动画问题的调试。
现在,您会发现帧查看器中的帧与 Vsync 边界更好地对齐,并根据状态进行颜色编码。目前,我们仍无法完全直观地呈现上述所有细微差别,但我们计划在不久的将来添加更多相关功能。
Chrome 跟踪
最后,借助 Chrome Tracing(深入了解细节的首选工具),您可以通过新的 Perfetto 界面 (about:tracing
) 记录“Web 内容渲染”轨迹,并深入了解 Chrome 的图形管道。这可能是一项艰巨的任务,但最近添加到 Chromium 中的一些功能可让这项任务变得更轻松。您可以查看帧的生命周期文档,大致了解可用的内容。
通过跟踪事件,您可以明确确定:
- 正在运行哪些动画(使用名为
TrackerValidation
的事件)。 - 获取动画帧的确切时间轴(使用名为
PipelineReporter
的事件)。 - 对于卡顿的动画更新,请确切找出是什么阻止了动画更快运行(使用
PipelineReporter
事件中的事件细分)。 - 对于输入驱动的动画,请查看获取视觉更新(使用名为
EventLatency
的事件)所需的时间。
后续步骤
网页指标计划旨在为在网络上打造出色的用户体验提供指标和指南。基于实验室的指标(例如 Total Blocking Time (TBT))对于发现和诊断潜在的互动性问题至关重要。我们计划设计一个类似的基于实验室的动画流畅度指标。
我们会继续探索基于各个动画帧数据设计综合指标的创意,并及时向您通报最新进展。
未来,我们还希望设计一些 API,以便在实地和实验室中高效地衡量真实用户的动画流畅度。 敬请关注该方面的最新动态!
反馈
我们很高兴看到 Chrome 中最近发布了许多用于衡量动画流畅度的改进和开发者工具。请试用这些工具,对动画进行基准比较,并告诉我们结果如何!
您可以向 web-vitals-feedback Google 群组发送评论,并在主题行中添加“[流畅度指标]”。我们非常期待听到您的想法!