了解如何衡量动画、如何考虑动画帧以及整体页面流畅度。
您可能遇到过网页在滚动或呈现动画时“卡顿”或“冻结”的情况。我们喜欢说这些体验不顺畅。为了解决此类问题,Chrome 团队一直在努力为我们的实验室工具添加更多动画检测支持,并不断改进 Chromium 中的渲染管道诊断功能。
我们想分享一些近期的进展、提供具体的工具指南,并讨论未来的动画流畅度指标的想法。一如既往,我们非常期待收到您的反馈。
本文将介绍以下三个主要主题:
- 快速了解动画和动画帧。
- 我们目前对衡量整体动画流畅度的想法。
- 以下是一些实用建议,可供您立即在实验室工具中加以利用。
什么是动画?
动画可让内容生动起来!通过让内容移动(尤其是在响应用户互动时),动画可以让体验感觉更自然、更易懂、更有趣。
但是,如果动画实现不当,或者添加的动画过多,可能会降低用户体验,使其完全没有乐趣。我们可能都曾与过度添加“实用”转换效果的界面互动过,这些效果在性能不佳时实际上会破坏用户体验。因此,有些用户实际上可能更喜欢减少动画,您应尊重用户的这种偏好设置。
动画的运作方式
简要回顾一下,渲染流水线由几个顺序的阶段组成:
- 样式:计算应用于元素的样式。
- 布局:为每个元素生成几何图形和位置。
- 绘制:将每个元素的像素填充到图层中。
- 合成:将层绘制到屏幕。
虽然有许多方法可以定义动画,但它们从根本上都通过以下某种方式运作:
- 调整布局属性。
- 调整绘制属性。
- 调整复合属性。
由于这些阶段是顺序的,因此请务必根据流水线更下方的属性来定义动画。更新在流程中发生得越早,费用就越高,并且不太可能顺利完成。(如需了解详情,请参阅渲染性能)。
虽然为布局属性添加动画效果很方便,但这样做会产生成本,即使这些成本并不明显。应尽可能根据复合属性更改来定义动画。
定义声明式 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 线程更新
动画帧更新不是布尔值。帧并非只能完全丢弃或完全呈现。导致动画帧部分 呈现的原因有很多。换句话说,它可以同时包含一些过时内容,同时也包含一些新的视觉更新。
最常见的示例是,浏览器无法在帧期限内生成新的主线程更新,但确实有新的合成器线程更新(例如前面的线程滚动示例)。
建议使用声明式动画为复合属性添加动画的一个重要原因是,这样可以让动画完全由合成器线程驱动,即使主线程繁忙也不例外。此类动画可以继续高效并行生成视觉更新。
另一方面,在某些情况下,主线程更新最终可能会可供呈现,但前提是错过了几个帧期限。此时,浏览器将有一些新更新,但可能不是最新版本。
一般来说,我们将包含部分新视觉更新(而非所有新视觉更新)的帧视为部分帧。部分帧非常常见。理想情况下,部分更新至少应包含最重要的视觉更新,例如动画,但只有在动画由 compositor 线程驱动时才能实现这一点。
缺少绘制更新
另一种类型的部分更新是,图片等媒体未能及时完成解码和光栅化,无法在帧呈现时使用。
或者,即使网页完全静态,浏览器在快速滚动期间也可能无法及时渲染视觉更新。这是因为,系统可能会舍弃超出可见视口的内容的像素呈现,以节省 GPU 内存。渲染像素需要时间,在发生大范围滚动(例如手指快速滑动)后,渲染所有内容可能需要的时间会超过一个帧。这通常称为棋盘模式。
在每次帧渲染机会中,您都可以跟踪最新视觉更新实际显示在屏幕上的比例。衡量在许多帧(或时间)内执行此操作的能力通常称为帧吞吐量。
如果 GPU 确实陷入了瓶颈,浏览器(或平台)甚至可能会开始限制其尝试进行视觉更新的速率,从而降低有效帧速率。虽然从技术层面来说,这可以减少丢弃的帧更新数量,但从视觉上看,帧吞吐量仍会降低。
不过,并非所有类型的低帧速率都是不好的。如果网页大部分时间处于空闲状态且没有任何动画在运行,那么低帧速率的视觉效果与高帧速率一样出色(而且还能节省电量!)。
那么,帧吞吐量在什么情况下很重要?
检测动画
高帧速率至关重要,尤其是在包含重要动画的期间。不同的动画类型将取决于特定线程(主线程、合成器或工作器)中的视觉更新,因此其视觉更新取决于该线程是否在截止期限内提供更新。每当有依赖于该线程更新的有效动画时,我们就说给定线程会影响流畅度。
有些类型的动画比其他类型的动画更容易定义和检测。与以动画可用样式属性的定期更新方式实现的 JavaScript 驱动型动画相比,声明式动画(即用户输入驱动型动画)的定义更清晰。
即使使用 requestAnimationFrame()
,您也不能总是假定每个 rAF 调用都一定会产生视觉更新或动画。例如,仅出于跟踪帧速率目的而使用 rAF 轮询(如上所示)本身不应影响流畅度测量,因为没有视觉更新。
质量与数量
最后,检测动画和动画帧更新仍然只是故事的一部分,因为它只捕获动画更新的数量,而非质量。
例如,您在观看视频时可能会看到稳定的 60 fps 帧速率。从技术层面来说,这完全是流畅的,但视频本身可能码率较低,或者存在网络缓冲问题。动画流畅度指标不会直接捕获此问题,但用户可能仍会感到不舒服。
或者,利用 <canvas>
的游戏(甚至可能使用屏幕外画布等技术来确保稳定的帧速率)在技术上可能在动画帧方面完全流畅,但却无法将高质量游戏资源加载到场景中,或者会出现渲染工件。
当然,某个网站可能只是包含一些非常糟糕的动画 🙂?
我想,它们在当时应该很酷!
单个动画帧的状态
由于帧可能会部分呈现,或者丢帧的方式可能不会影响流畅度,因此我们开始将每个帧视为具有完整性或流畅度得分。
下面是解释单个动画帧状态的各种方式,从最好到最差情况排列:
不希望更新 | 空闲时间,重复上一帧。 |
已完全呈现 | 主线程更新已在截止期限内提交,或者不需要进行主线程更新。 |
已部分提交 | 仅限合成器;延迟的主线程更新没有任何视觉变化。 |
已部分提交 | 仅限合成器;主线程进行了视觉更新,但该更新不包含影响流畅度的动画。 |
已部分提交 | 仅限合成器;主线程进行了影响流畅度的视觉更新,但之前的过时帧已到达并被使用。 |
已部分提交 | 仅限合成器;没有所需的主要更新,并且合成器更新包含会影响流畅度的动画。 |
已部分提交 | 仅限合成器,但合成器更新没有影响流畅度的动画。 |
丢弃的帧 | 无更新。不需要任何 compositor 更新,并且 main 延迟了。 |
丢弃的帧 | 我们希望进行 compositor 更新,但该更新已延迟。 |
过时帧 | 需要更新,它由渲染程序生成,但 GPU 仍未在 vsync 截止日期之前显示它。 |
可以将这些状态转换为某种得分。或许可以通过以下方式解读此得分:将其视为用户察觉到问题的概率。单个帧丢失可能不太明显,但连续丢失多个帧会影响流畅度,这一点肯定是显而易见的!
综合应用:丢帧百分比指标
虽然有时可能需要深入了解每个动画帧的状态,但只为体验分配一个快速“一目了然”的得分也很有用。
由于帧可能会部分呈现,并且即使完全跳过帧更新实际上也可能不会影响流畅度,因此我们不想过于关注帧数,而更关注浏览器在重要时刻无法提供视觉上完整更新的程度。
心智模型应从以下方面转变:
- 每秒帧数,用于
- 检测缺失和重要的动画更新,以便
- 指定时间段内的流量下降百分比。
重要的是:等待重要更新所占的时间比例。我们认为,这与用户在实践中体验网页内容流畅度的自然方式相符。到目前为止,我们一直在使用以下指标作为一组初始指标:
- 平均丢帧百分比:整个时间轴中所有非空闲动画帧的丢帧百分比
- 丢帧百分比的最差情况:根据 1 秒滑动时间窗口测量得出。
- 丢帧百分比的第 95 百分位:在 1 秒的滑动时间窗口内测量得出。
目前,您可以在某些 Chrome 开发者工具中找到这些得分。虽然这些指标仅侧重于整体帧吞吐量,但我们还会评估其他因素,例如帧延迟时间。
您可以在开发者工具中亲自试用!
性能平视显示
Chromium 有一个隐藏在标志 (chrome://flags/#show-performance-metrics-hud
) 后面的简洁性能 HUD。在其中,您可以找到核心网页指标等内容的实时得分,以及一些基于一段时间内的帧丢失百分比的动画流畅度实验性定义。
帧渲染统计信息
通过“渲染”设置在 DevTools 中启用“帧渲染统计信息”,以查看新动画帧的实时视图。这些帧会采用颜色编码,以区分部分更新和完全丢弃的帧更新。报告的帧速率仅适用于完全呈现的帧。
开发者工具性能配置文件录制中的帧查看器
DevTools 性能面板中一直都有帧查看器。不过,它与现代渲染流水线的实际运作方式略有不同。最近,我们进行了大量改进,即使在最新的 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 群组,并在主题行中添加“[Smoothness Metrics]”。我们非常期待听取您的意见!