了解如何衡量动画、如何考虑动画帧以及整体页面流畅度。
您可能遇到过网页在滚动或呈现动画时“卡顿”或“冻结”的情况。我们喜欢说这些体验不顺畅。为了解决此类问题,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]”。我们非常期待听取您的意见!