关于动画流畅性指标

了解如何测量动画、如何看待动画帧以及网页的整体流畅性。

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

您可能遇到过页面在滚动或动画期间“卡顿”或“卡住”的情况。可以说,这些体验并不顺畅。为了解决此类问题,Chrome 团队一直致力于为我们的实验室工具增加用于动画检测的更多支持,并稳定改进 Chromium 中的渲染流水线诊断。

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

这篇博文将介绍三个主要主题:

  • 快速浏览动画和动画帧。
  • 我们目前对衡量整体动画平滑度的想法。
  • 下面是一些您在实验室工具中可以运用的实用建议。

什么是动画?

动画让内容更生动有趣!通过使内容移动,尤其是在响应用户互动时,动画可以让人感觉更自然、更易于理解和更有趣。

但是,如果动画实现不当,或者添加过多动画,可能会降低游戏体验,使其显得毫无趣味。我们可能都使用过一个添加了太多“有用”过渡效果的接口,这种效果在性能不佳时实际上会给体验带来不利影响。因此,有些用户实际上可能更喜欢“减少动作”,这是您应该尊重的用户偏好。

动画如何运作?

快速回顾一下,渲染流水线包括几个依序的阶段:

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

虽然定义动画的方法有很多,但从根本上来说,它们都是通过以下方法之一实现的:

  • 调整布局属性。
  • 调整绘制属性。
  • 调整复合属性。

由于这些阶段是依序进行的,因此根据更靠近流水线的属性定义动画非常重要。更新过程中进行的时间越早,费用越高,顺利完成的可能性就越小。(如需了解详情,请参阅渲染性能。)

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

如需确保流畅高效的动画,首先要做的就是定义声明性 CSS 动画或使用网页动画,并确保为复合属性添加动画效果。但仍然无法保证流畅性,因为即使是高效的 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() 在连续使用时调度较长的空闲块(超出单个帧的块)。
  • 同样,缺少长空闲块会导致浏览器无法调度其他长时间运行的任务(例如较长的垃圾回收和其他后台或推测性工作)。
  • 如果轮询是开启和关闭的,那么您就会错过超出帧预算的情况。
  • 如果浏览器使用可变的更新频率(例如,由于电源或可见性状态),轮询会报告假正例。
  • 最重要的是,它实际上并不会捕获所有类型的动画更新!

主线程上的工作过多可能会影响查看动画帧的功能。请参阅卡顿示例,了解当主线程上工作太多(如布局)时,由 rAF 驱动的动画会如何导致丢帧、减少 rAF 回调并降低 FPS。

当主线程变得卡顿时,视觉更新开始卡顿。造成卡顿!

许多测量工具都非常注重主线程及时产出的能力以及动画帧顺畅运行的能力。但这还不是一切!请参考以下示例:

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

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

例如,尽管主线程上包含许多丢失的帧,但在合成器线程上仍有许多成功传送的滚动帧。这个耗时较长的任务完成后,主线程绘制更新没有任何视觉变化。rAF 轮询会建议丢帧为 0,但从视觉上看,用户看不出任何变化!

对于动画帧,情况就没那么简单。

动画帧:重要的更新

上面的示例展示了不仅仅是 requestAnimationFrame() 的故事情节。

那么,动画更新和动画帧何时很重要?以下是我们正在考虑的一些标准,希望您收到关于这些标准的反馈:

  • 主线程和合成器线程更新
  • 缺少渲染更新
  • 检测动画
  • 质量与数量

主线程和合成器线程更新

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

最常见的一个例子是,浏览器无法在帧的时限内生成新的主线程更新,但确实发生了新的合成器线程更新(如之前的线程式滚动示例)。

建议使用声明式动画为复合属性添加动画效果的一个重要原因是,即使主线程处于忙碌状态,动画也可以完全由合成器线程驱动。这些类型的动画可以继续高效且并行地生成视觉更新。

另一方面,在某些情况下,主线程更新最终才可供呈现,但仅在错过几个帧的时限后才可用。在这里,浏览器会进行一些新的更新,但可能不是最新版本

一般来说,我们会将包含一些新视觉更新(但不包含所有新视觉更新)的帧视为部分帧。部分帧相当常见。理想情况下,部分更新至少包含最重要的视觉更新,例如动画,但只有在动画由合成器线程驱动时才会发生这种情况。

缺少渲染更新

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

或者,即使网页完全静态,在快速滚动过程中,浏览器仍可能无法呈现可视更新。这是因为,在可见视口之外以像素呈现的内容可能会被舍弃,以节省 GPU 内存。渲染像素需要一些时间,并且在大幅度滚动(例如滑动手指)后,渲染所有内容所需的时间可能比单个帧更长。这通常称为“棋盘式”

有了每一帧渲染机会,系统就可以跟踪实际呈现到屏幕上的最新视觉更新有多少。衡量跨多个帧(或时间)执行此操作的能力通常称为“帧吞吐量”。

如果 GPU 确实无法运行,浏览器(或平台)甚至可能会开始限制尝试进行视觉更新的速率,从而降低有效帧速率。虽然从技术层面来讲,这样做可以减少丢帧更新的次数,但从视觉上看,这仍然会表现为较低的帧吞吐量。

然而,并非所有类型的低帧吞吐量都是糟糕的。如果网页大多处于空闲状态,并且没有有效的动画,那么较低的帧速率与高帧速率一样具有视觉吸引力(而且可以节省电量!)。

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

检测动画

高帧吞吐量非常重要,尤其是在具有重要动画的时段。不同的动画类型将依赖于来自特定线程(主线程、合成器或工作器)的视觉更新,因此其视觉更新取决于该线程在时限内提供更新。我们说,每当存在依赖于某个线程更新的活跃动画时,给定线程都会影响流畅性。

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

即使使用 requestAnimationFrame(),您也无法始终假定每次 rAF 调用都会生成视觉更新或动画。例如,仅仅为了跟踪帧速率而使用 rAF 轮询(如上所示)本身不应影响流畅度测量,因为没有视觉更新。

质量与数量

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

例如,您可能会在观看视频时看到稳定的帧速率为 60 fps。从技术上讲,该过程非常流畅,但视频本身的比特率可能较低,或者网络缓冲存在问题。动画平滑度指标不会直接捕获这一点,但可能仍然会对用户造成干扰。

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

当然,网站中可能只是有一些非常糟糕的动画 🙂?

在建的老学校 GIF

那时他们真的很酷!

单个动画帧的状态

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

以下是我们解释单个动画帧状态的一系列方式,按从高到低的顺序排列:

无需更新 闲置时间,重复上一帧。
完整呈现 主线程更新要么在截止时间内提交,要么不需要主线程更新。
部分呈现 仅限合成器;延迟的主线程更新没有视觉变化。
部分呈现 仅限合成器;主线程进行了视觉更新,但该更新不包含影响流畅性的动画。
部分呈现 仅限合成器;主线程具有影响平滑度的视觉更新,但是先前的过时帧到达并被使用。
部分呈现 仅限合成器;没有所需的主更新,并且合成器更新具有会影响流畅度的动画。
部分呈现 仅限合成器,但合成器更新没有影响平滑度的动画。
丢帧 无更新。无需更新合成器,导致 main 延迟。
丢帧 需要对合成器更新,但它被延迟了。
过时帧 需要更新,更新是由渲染程序生成的,但 GPU 在 Vsync 截止时间之前仍未提供更新。

我们有可能将这些状态变成某种分数。或许一种解读此得分的方式是将其视为用户可观察到的概率。单个丢弃的帧可能不是很明显,但一系列影响行平滑性的一系列丢帧确实是可观察到的!

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

虽然有时需要深入了解每个动画帧的状态,但仅为体验分配一个快速的“概览”评分也很有用。

由于帧可能会部分呈现,而且即使完全跳过的帧更新可能实际上并不会影响流畅性,因此我们希望减少仅计算帧数,而将更多精力放在浏览器无法在必要时提供完全视觉完整更新的程度上。

这种思维模式应该从:

  1. 每秒帧数
  2. 检测缺失和重要的动画更新
  3. 给定时间段内的下降百分比

重要的是:等待重要更新的时间比例。我们认为,这很符合用户在实际操作中体验 Web 内容流畅性的自然方式。到目前为止,我们一直在使用以下各项作为初始指标集:

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

您现在可以在部分 Chrome 开发者工具中找到这些得分。虽然这些指标仅关注整体帧吞吐量,但我们也在评估其他因素,例如帧延迟时间。

在开发者工具中亲自尝试一下!

性能平视显示 (HUD)

Chromium 有一个整洁的性能 HUD,隐藏在标志 (chrome://flags/#show-performance-metrics-hud) 后面。在该标志中,您可以找到 Core Web Vitals 等方面的实时得分,以及基于一段时间内的丢帧百分比提供的动画流畅性的一些实验性定义。

性能平视显示 (HUD)

帧渲染统计信息

在开发者工具中通过渲染设置启用“帧渲染统计信息”,以查看新动画帧的实时视图,这些动画帧已进行颜色编码,用于区分部分更新与完全丢弃的帧更新。报告的 fps 仅适用于完全呈现的帧。

帧渲染统计信息

开发者工具性能配置文件记录中的 Frame Viewer

DevTools 性能面板一直有一个 Frames 查看器。不过,它与现代渲染管道的实际运作方式有点不同步。我们近期做出了很多改进,即使是在最新的 Chrome Canary 版中也是如此,我们认为这些改进可以大大简化动画问题的调试。

现在,您会发现帧查看器中的帧更符合 Vsync 边界,并且根据状态进行颜色编码。上述所有细微差别仍没有全面的可视化效果,但我们计划在不久的将来添加更多的可视化效果。

Chrome 开发者工具中的框架查看器

Chrome 跟踪

最后,借助 Chrome Tracing(一种深入探究细节的首选工具),您可以通过新的 Perfetto 界面(或 about:tracing)录制“Web 内容渲染”跟踪记录,并深入了解 Chrome 的图形管道。这项任务是一项艰巨的任务,但我们最近在 Chromium 中添加了一些功能,以简化这项工作。您可以大致了解帧的生命周期文档中提供的内容。

通过跟踪事件,您可以明确地确定:

  • 哪些动画正在运行(使用名为 TrackerValidation 的事件)。
  • 获取动画帧的确切时间轴(使用名为 PipelineReporter 的事件)。
  • 对于卡顿的动画更新,请确切了解是什么阻止动画加速运行(使用 PipelineReporter 事件中的事件细分)。
  • 对于输入驱动的动画,请查看获取视觉更新所需的时间(使用名为 EventLatency 的事件)。

Chrome 跟踪流水线报告程序

后续步骤

网页指标计划旨在提供相关指标和指南,帮助您在网络上打造出色的用户体验。总阻塞时间 (TBT)基于实验室的指标对于发现和诊断潜在的互动问题至关重要。我们计划设计一个类似的基于实验室的动画流畅性指标。

在我们继续研究如何基于各个动画帧数据设计综合指标时,我们会及时通知您。

将来,我们还希望设计一些 API,以便在现场和实验室中为真实用户有效地衡量动画平滑度。同时,敬请关注那里的最新动态!

反馈

我们非常期待 Chrome 中用于衡量动画流畅度的最新改进和开发者工具。请试用这些工具,对动画进行基准化分析,并告诉我们其使用方向!

您可以将您的意见发送到 web-vitals-feedback Google 网上论坛,并在主题行中包含“[流畅度指标]”。我们非常期待收到您的宝贵意见!