关于动画流畅性指标

了解如何衡量动画、如何考虑动画帧以及整体页面流畅度。

Behdad Bakhshinategh
Behdad Bakhshinategh
Jonathan Ross
Jonathan Ross
Michal Mocny
Michal Mocny

您可能遇到过页面在滚动或动画期间出现“卡顿”或“卡住”的情况。我们喜欢说这些体验不顺畅。为了解决此类问题,Chrome 团队一直在努力为我们的实验室工具添加更多动画检测支持,并不断改进 Chromium 中的渲染管道诊断功能。

我们想分享一些近期的进展、提供具体的工具指南,并讨论未来的动画流畅度指标的想法。一如既往,我们非常期待收到您的反馈

本博文将涵盖三大主题:

  • 快速了解动画和动画帧。
  • 我们目前对衡量整体动画流畅度的想法。
  • 以下是一些实用建议,供您立即在实验室工具中加以利用。

什么是动画?

动画可让内容生动起来!通过让内容移动(尤其是在响应用户互动时),动画可以让体验感觉更自然、更易懂、更有趣。

但是,如果动画实现不当,或者只是添加太多动画,则可能会降低体验,使动画显然毫无趣味。我们可能都曾与过度添加“实用”转换效果的界面互动过,这些效果在性能不佳时实际上会破坏用户体验。因此,有些用户实际上可能更喜欢减少动画效果,您应尊重用户的这一偏好设置。

动画的运作方式

简要回顾一下,渲染流水线由几个连续的阶段组成:

  1. 样式:计算应用于元素的样式。
  2. 布局:为每个元素生成几何图形和位置。
  3. 绘制:将每个元素的像素填充到图层中。
  4. 复合:将层绘制到屏幕上。

虽然有许多方法可以定义动画,但它们从根本上都通过以下某种方式运作:

  • 调整布局属性。
  • 调整绘制属性。
  • 调整 composite 属性。

由于这些阶段是顺序的,因此请务必根据流水线下游的属性来定义动画。更新在流程中发生得越早,费用就越高,并且不太可能顺利完成。(如需了解详情,请参阅渲染性能)。

虽然为布局属性添加动画效果很方便,但这样做会产生成本,即使这些成本并不明显。应尽可能根据复合属性更改来定义动画。

定义声明式 CSS 动画或使用 Web 动画,并确保为复合属性添加动画效果,是确保动画流畅高效的良好开端。不过,仅此一项并不能保证流畅,因为即使高效的 Web 动画也存在性能限制。因此,衡量效果始终很重要!

什么是动画帧?

网页的视觉表示更新需要一些时间才能显示。视觉变化会导致新的动画帧,最终在用户的显示屏上呈现。

按某个时间间隔显示更新,以便对视觉更新进行批量处理。许多显示屏会按固定的时间间隔进行更新,例如每秒 60 次(即 60 Hz)。一些较新的显示屏可以提供更高的刷新率(90-120 Hz 的刷新率越来越常见)。通常,这些显示屏可以根据需要主动调整刷新率,甚至提供完全可变的帧速率。

任何应用(例如游戏或浏览器)的目标都是每次都能在截止期限内处理所有这些批量视觉更新,并生成视觉上完整的动画帧。请注意,此目标与其他重要的浏览器任务(例如快速从网络加载内容或高效执行 JavaScript 任务)完全不同。

在某些情况下,在显示屏分配的截止期限内完成所有视觉更新可能非常困难。发生这种情况时,浏览器会丢弃帧。屏幕不会变黑,只是重复播放。您会看到相同的视觉更新(上一个帧机会中显示的动画帧)稍长一些时间。

这种情况其实很常见!甚至可能不会被察觉到,尤其是对于静态或类似文档的内容(这类内容在 Web 平台上尤为常见)。只有在有重要的视觉更新(例如动画)时,掉帧才会明显,因为我们需要稳定的动画更新才能显示流畅的动作。

哪些因素会影响动画帧?

Web 开发者可以极大地影响浏览器快速高效地渲染和呈现视觉更新的能力!

一些示例:

  • 使用过大或占用大量资源的内容,导致无法在目标设备上快速解码。
  • 使用过多层,需要过多的 GPU 内存。
  • 定义过于复杂的 CSS 样式或 Web 动画。
  • 使用会停用快速渲染优化的设计反模式。
  • 主线程上的 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() 在连续使用时无法调度较长的空闲块(超出单个帧的块)。
  • 同样,缺少长时间的空闲块会导致浏览器无法调度其他长时间运行的任务(例如更长时间的垃圾回收以及其他后台或推测性工作)。
  • 如果轮询处于开启和关闭状态切换,则会错过帧预算超出的情况。
  • 如果浏览器使用可变更新频率(例如,由于电源或可见性状态),轮询将报告误报。
  • 最重要的是,它实际上无法捕获所有类型的动画更新!

在主线程上执行太多工作可能会影响查看动画帧的能力。请查看卡顿示例,了解当主线程上有太多工作(例如布局)时,由 RAF 驱动的动画如何导致帧丢失、rAF 回调减少以及 FPS 降低。

当主线程陷入困境时,视觉更新会开始卡顿。这很卡顿!

许多衡量工具都非常注重主线程是否能够及时让出,以及动画帧是否能够顺畅运行。但这还不是全部!请参考以下示例:

上方视频展示了一个会定期向主线程注入长任务的网页。这些耗时任务会完全破坏页面提供某些类型的视觉更新的能力,您可以在左上角看到 requestAnimationFrame() 报告的 FPS 相应下降到 0。

尽管有这些耗时任务,页面仍会继续流畅滚动。这是因为在现代浏览器中,滚动通常是线程化的,完全由合成器驱动。

此示例在主线程上同时包含许多丢弃的帧,但在合成器线程上仍有许多已成功传递的滚动帧。长时间运行的任务完成后,主线程绘制更新将无法提供任何视觉变化。rAF 轮询建议将帧率降至 0,但在视觉上,用户无法察觉到任何差异!

对于动画帧,情况并不那么简单。

动画帧:重要的更新

上述示例展示了故事不仅仅包含 requestAnimationFrame()

那么,动画更新和动画帧何时适用?以下是我们正在考虑的一些标准,希望能收到您的反馈:

  • 主线程和 compositor 线程更新
  • 缺少绘制更新
  • 检测动画
  • 质量还是数量

主线程和 compositor 线程更新

动画帧更新不是布尔值。帧并非只能完全丢弃或完全呈现。导致动画帧部分呈现的原因有很多。换句话说,它可以同时包含一些过时内容,同时也包含一些新的视觉更新

最常见的一个例子是,浏览器无法在帧截止时间内生成新的主线程更新,但进行了新的合成器线程更新(例如前面的线程式滚动示例)。

建议使用声明式动画为复合属性添加动画的一个重要原因是,这样可以让动画完全由合成器线程驱动,即使主线程繁忙也不例外。这些类型的动画可以继续高效、并行地产生视觉更新。

另一方面,在某些情况下,主线程更新最终可能会可供呈现,但前提是错过了几个帧期限。此时,浏览器将有一些新更新,但可能不是最新版本。

一般来说,我们将包含部分新视觉更新(而非所有新视觉更新)的帧视为部分帧。部分帧是相当常见的。理想情况下,部分更新至少应包含最重要的视觉更新,例如动画,但只有在动画由合成器线程驱动时才能实现这一点。

缺少绘制更新

另一种类型的部分更新是图片等媒体尚未及时完成解码和光栅化以进行帧呈现。

或者,即使网页完全是静态的,在快速滚动过程中,浏览器仍可能会落后于视觉更新呈现速度。这是因为,为了节省 GPU 内存,系统可能会舍弃超出可见视口的内容的像素呈现。渲染像素需要时间,在发生大范围滚动(例如手指快速滑动)后,渲染所有内容可能需要的时间会超过一个帧。这通常称为“棋盘”

在每次帧渲染机会中,您都可以跟踪最新视觉更新实际显示在屏幕上的比例。衡量在许多帧(或时间)内执行此操作的能力通常称为帧吞吐量

如果 GPU 确实陷入了瓶颈,浏览器(或平台)甚至可能会开始限制其尝试进行视觉更新的速率,从而降低有效帧速率。虽然从技术层面来说,这可以减少丢弃的帧更新数量,但从视觉上看,帧吞吐量仍会降低。

不过,并非所有类型的低帧速率都是不好的。如果页面几乎处于空闲状态,并且没有活动的动画,则低帧速率与高帧速率在视觉上同样具有吸引力(而且可以节省电量!)。

那么,帧吞吐量在什么情况下很重要?

检测动画

高帧速率至关重要,尤其是在包含重要动画的期间。不同的动画类型将取决于特定线程(主线程、合成器或工作器)中的视觉更新,因此其视觉更新取决于该线程是否在截止期限内提供更新。我们说,只要有依赖于该线程更新的活动动画,给定线程就会影响流畅度

有些类型的动画比其他类型的动画更容易定义和检测。与以动画可用样式属性的定期更新方式实现的 JavaScript 驱动型动画相比,声明式动画(即用户输入驱动型动画)的定义更清晰。

即使使用 requestAnimationFrame(),您也不能总是假定每个 rAF 调用都一定会产生视觉更新或动画。例如,仅出于跟踪帧速率目的而使用 rAF 轮询(如上所示)本身不应影响流畅度测量,因为没有视觉更新。

质量与数量

最后,检测动画和动画帧更新仍然只是故事的一部分,因为它只捕获动画更新的数量,而非质量。

例如,您在观看视频时可能会看到稳定的 60 fps 帧速率。从技术层面来说,这完全是流畅的,但视频本身可能码率较低,或者存在网络缓冲问题。动画流畅度指标不会直接捕获此问题,但用户可能仍会感到不舒服。

或者,利用 <canvas> 的游戏(可能甚至使用屏幕外画布等技术来确保稳定的帧速率)在技术上可能在动画帧方面完全流畅,但却无法将高品质游戏资源加载到场景中,或者会出现渲染工件。

当然,某个网站可能只是包含一些非常糟糕的动画 🙂

建设中的老派 GIF

我想,它们在当时应该很酷!

单个动画帧的状态

由于帧可能会部分呈现,或者丢帧的方式可能不会影响流畅度,因此我们开始将每个帧视为具有完整性或流畅度得分。

以下是我们解释单个动画帧状态的一系列方式(按照从最好到最差的顺序排列):

不希望更新 空闲时间,重复上一帧。
已完全呈现 主线程更新已在截止期限内提交,或者不需要进行主线程更新。
已部分提交 仅限合成器;延迟的主线程更新没有任何视觉变化。
已部分提交 仅限合成器;主线程进行了视觉更新,但该更新不包含影响流畅度的动画。
已部分提交 仅限合成器;主线程具有影响流畅度的视觉更新,但系统到达并使用了之前的过时帧。
已部分提交 仅限合成器;没有所需的主要更新,并且合成器更新包含会影响流畅度的动画。
已部分提交 仅限合成器,但合成器更新没有影响流畅度的动画。
丢弃的帧 无更新。不需要任何 compositor 更新,并且 main 延迟了。
丢帧 我们希望进行 compositor 更新,但该更新已延迟。
过时帧 需要更新,它由渲染程序生成,但 GPU 仍未在 vsync 截止日期之前显示它。

您可以将这些状态转换为一定的分数。为了解读此得分,一种方法是将其视为用户可观察到的概率。单个帧丢失可能不太明显,但连续丢失多个帧会影响流畅度,这一点肯定是显而易见的!

综合应用:丢帧百分比指标

虽然有时可能需要深入了解每个动画帧的状态,但只为体验分配一个快速“一目了然”的得分也很有用。

由于帧可能会部分呈现,并且即使完全跳过帧更新实际上也可能不会影响流畅度,因此我们不想过于关注帧数,而更关注浏览器在重要时刻无法提供视觉上完整更新的程度

心理模型应从:

  1. 每秒帧数,用于
  2. 检测缺失的重要动画更新,
  3. 指定时间段内的流量下降百分比

重要的是:等待重要更新所占的时间比例。我们认为,这与用户在实践中体验网页内容流畅度的自然方式相符。到目前为止,我们一直使用以下指标作为初始指标集:

  • 平均丢帧百分比:整个时间轴中所有非空闲动画帧的丢帧百分比
  • 丢帧百分比的最差情况:根据 1 秒滑动时间窗口测量得出。
  • 丢弃的帧百分比的第 95 个百分位:根据 1 秒的滑动时间范围测量。

目前,您可以在一些 Chrome 开发者工具中查看这些得分。虽然这些指标仅侧重于整体帧吞吐量,但我们还会评估其他因素,例如帧延迟时间。

您可以在开发者工具中亲自试用!

性能平视显示

Chromium 有一个隐藏在标志 (chrome://flags/#show-performance-metrics-hud) 后面的简洁性能 HUD。在其中,您可以找到核心网页指标等内容的实时得分,以及一些基于一段时间内的帧丢失百分比的动画流畅度实验性定义。

性能平视显示

帧渲染统计信息

通过“渲染”设置在 DevTools 中启用“帧渲染统计信息”,以查看新动画帧的实时视图。这些帧会采用颜色编码,以区分部分更新和完全丢弃的帧更新。报告的帧速率仅适用于完全呈现的帧。

帧渲染统计信息

开发者工具性能配置文件录制中的帧查看器

DevTools 性能面板中一直有一个帧查看器。不过,它与现代渲染流水线的实际运作方式略有不同。最近,我们进行了大量改进,即使在最新的 Chrome Canary 中,也能大大简化动画问题的调试。

现在,您会发现帧查看器中的帧与 vsync 边界更加契合,并且会根据状态采用颜色编码。上述所有细微差别仍未完全可视化,但我们计划近期内添加更多细微差别。

Chrome 开发者工具中的 Frame Viewer

Chrome 跟踪记录

最后,借助 Chrome Tracing(深入探究细节的首选工具),您可以通过新的 Perfetto 界面(或 about:tracing)记录“Web 内容呈现”轨迹,并深入了解 Chrome 的图形管道。这项任务可能令人望而却步,但 Chromium 中最近添加了一些功能,可以让您更轻松地完成此任务。您可以参阅帧生命周期文档,大致了解可用功能。

通过轨迹事件,您可以明确确定:

  • 正在运行哪些动画(使用名为 TrackerValidation 的事件)。
  • 获取动画帧的确切时间轴(使用名为 PipelineReporter 的事件)。
  • 对于动画更新卡顿问题,请准确找出导致动画无法更快运行的原因(使用 PipelineReporter 事件中的事件细分)。
  • 对于输入驱动型动画,请查看获取视觉更新所需的时间(使用名为 EventLatency 的事件)。

Chrome Tracing 流水线报告程序

后续步骤

网页指标计划旨在提供指标和指南,帮助您打造出色的 Web 用户体验。实验室指标(例如 Total Blocking Time [TBT])对于发现和诊断潜在的互动性问题至关重要。我们计划设计一个类似的基于实验室的动画平滑度指标。

我们会继续探索设计基于各个动画帧数据的全面指标的想法,并及时通知您最新进展。

未来,我们还希望设计 API,以便在现场和实验室中高效地衡量动画流畅度。敬请关注此页面的最新动态!

反馈

对于 Chrome 中新增的用于衡量动画流畅度的所有最新改进和开发者工具,我们感到非常兴奋。请试用这些工具、对您的动画进行基准测试,并告诉我们结果!

您可以将评论发送到 web-vitals-feedback Google 群组,并在主题行中包含“[Smoothness Metrics]”。我们非常期待听取您的意见!