简介
您希望 Web 应用在执行动画、过渡和其他小型界面效果时能够快速响应且流畅。确保这些效果流畅无卡顿,可以带来“原生”体验,否则会让人感觉笨拙、粗糙。
这是系列文章中的第一篇,该系列文章涵盖了浏览器中的渲染性能优化。首先,我们将介绍为什么流畅的动画难以实现、需要采取哪些措施才能实现流畅的动画,以及一些简单的最佳实践。其中许多想法最初是在“Jank Busters”中提出的,这是 Nat Duca 和我在今年的 Google I/O 大会上发表的演讲(视频)。
V-sync 简介
PC 游戏玩家可能熟悉这个术语,但在网络上并不常见:什么是 v-sync?
以手机显示屏为例:它会定期刷新,通常(但并非总是)每秒刷新约 60 次。V-sync(或垂直同步)是指仅在屏幕刷新之间生成新帧的做法。您可以将其视为将数据写入屏幕缓冲区的进程与读取该数据并将其显示在屏幕上的操作系统之间的竞态条件。我们希望缓冲的帧内容在这些刷新之间发生变化,而不是在刷新期间发生变化;否则,显示器将显示一帧的一半和另一帧的一半,从而导致“画面撕裂”。
为了获得流畅的动画,您需要在每次屏幕刷新时准备好新帧。这会产生两个重大影响:帧时间(即帧需要准备就绪的时间)和帧预算(即浏览器生成帧所需的时间)。您只有在屏幕刷新之间的时间内才能完成一帧(在 60Hz 屏幕上约为 16 毫秒),并且您希望在上一帧显示在屏幕上后立即开始生成下一帧。
时机决定一切:requestAnimationFrame
许多 Web 开发者每 16 毫秒使用一次 setInterval 或 setTimeout 来创建动画。由于各种原因,这是一个问题(我们稍后会详细讨论),但特别值得关注的是:
- JavaScript 的计时器分辨率仅为几毫秒
- 不同设备具有不同的刷新率
回想一下上面提到的帧时间问题:您需要一个完整的动画帧,其中包含所有 JavaScript、DOM 操作、布局、绘制等,以便在下一次屏幕刷新之前准备就绪。如果计时器分辨率较低,则可能很难在下一次屏幕刷新之前完成动画帧,但屏幕刷新率的变化使得固定计时器无法实现这一点。无论计时器间隔是多少,您都会慢慢偏离帧的时间窗口,最终导致丢帧。即使计时器以毫秒级精度触发,也会发生这种情况,但事实并非如此(正如开发者所发现的那样)-- 计时器分辨率因机器是使用电池还是插电而异,可能会受到后台标签页占用资源等因素的影响。即使这种情况很少见(例如,由于您延迟了一毫秒,每 16 帧就会发生一次),您也会注意到:您每秒会丢弃几帧。您还需要执行一些工作来生成从未显示的帧,这会浪费电量和 CPU 时间,而这些资源本可以用于在应用中执行其他操作。
不同的显示屏具有不同的刷新率:60Hz 很常见,但有些手机是 59Hz,有些笔记本电脑在低功耗模式下会降至 50Hz,有些桌面显示器是 70Hz。
在讨论渲染性能时,我们往往会关注每秒帧数 (FPS),但方差可能是一个更大的问题。我们的眼睛会注意到动画中微小、不规则的卡顿,这些卡顿可能是由于动画时间设置不当造成的。
要获得时间正确的动画帧,可以使用 requestAnimationFrame。使用此 API 时,您是在向浏览器请求动画帧。当浏览器即将生成新帧时,系统会调用您的回调。无论刷新率如何,都会出现这种情况。
requestAnimationFrame 还具有其他一些不错的属性:
- 后台标签页中的动画会暂停,从而节省系统资源并延长电池续航时间。
- 如果系统无法以屏幕的刷新率处理渲染,则可以限制动画并降低回调频率(例如,在 60Hz 屏幕上每秒 30 次)。虽然这会将帧速率降低一半,但能保持动画的一致性。如上所述,我们的眼睛对帧速率变化的敏感度远高于对帧速率本身的敏感度。稳定的 30Hz 看起来比每秒丢失几帧的 60Hz 更好。
requestAnimationFrame 已经广为人知,因此请参阅 Creative JS 的这篇文章等文章,了解更多相关信息,但这是实现流畅动画的重要第一步。
帧预算
由于我们希望在每次屏幕刷新时都准备好新帧,因此只有在刷新之间的时间内才能完成创建新帧的所有工作。在 60Hz 的显示屏上,这意味着我们有大约 16 毫秒的时间来运行所有 JavaScript、执行布局、绘制以及浏览器为输出帧而必须执行的其他任何操作。这意味着,如果 requestAnimationFrame 回调内的 JavaScript 运行时间超过 16 毫秒,您将无法及时生成帧以供 V-sync 使用!
16 毫秒的时间并不长。幸运的是,Chrome 的开发者工具可以帮助您跟踪在 requestAnimationFrame 回调期间是否超出了帧预算。
打开开发者工具时间轴并录制此动画的运行过程,很快就会发现我们在动画制作过程中超出了预算。在时间轴中,切换到“帧”并查看:
这些 requestAnimationFrame (rAF) 回调耗时超过 200 毫秒。这个时间太长了,根本无法做到每 16 毫秒就输出一帧!打开其中一个较长的 rAF 回调,即可了解内部情况:在本例中,有大量布局。
Paul 的视频更详细地介绍了导致重新布局的具体原因(读取 scrollTop)以及如何避免这种情况。但这里的重点是,您可以深入了解回调,并调查耗时如此之长的原因。
请注意 16 毫秒的帧时间。帧中的空白空间是您需要执行更多工作(或让浏览器在后台执行所需工作)的余量。留白是件好事。
其他卡顿来源
尝试运行 JavaScript 动画时,最容易出现的问题是其他内容可能会妨碍 rAF 回调,甚至导致其根本无法运行。即使您的 rAF 回调很精简,只需几毫秒即可运行,但其他活动(例如处理刚传入的 XHR、运行输入事件处理脚本或在计时器上运行计划更新)可能会突然出现并运行任意一段时间,而不会让出控制权。在移动设备上,有时处理这些事件可能需要数百毫秒,在此期间,动画将完全停滞。我们将这些动画卡顿称为卡顿。
虽然没有万能的方法可以避免这些情况,但您可以遵循一些架构最佳实践,为成功奠定基础:
- 请勿在输入处理程序中执行大量处理!在 onscroll 处理程序等期间执行大量 JS 或尝试重新排列整个页面是导致严重卡顿的常见原因。
- 尽可能将处理工作(即任何需要长时间运行的任务)推送到 rAF 回调或 Web Worker 中。
- 如果您将工作推送到 rAF 回调中,请尝试将其分块,以便您每帧仅处理一点点,或者将其延迟到重要动画结束之后,这样您就可以继续运行简短的 rAF 回调并流畅地呈现动画。
如需查看有关如何将处理推送到 requestAnimationFrame 回调(而非输入处理程序)的实用教程,请参阅 Paul Lewis 的文章使用 requestAnimationFrame 打造更精简、更强大、更快速的动画。
CSS 动画
在事件和 rAF 回调中,有什么比轻量级 JS 更好?无 JS。
前面我们说过,没有万全之策可以避免中断 rAF 回调,但您可以使用 CSS 动画来完全避免对它们的需求。特别是在 Chrome for Android 上(其他浏览器也在开发类似功能),CSS 动画具有一个非常理想的属性:即使 JavaScript 正在运行,浏览器通常也能运行 CSS 动画。
在上面关于卡顿的部分中,有一个隐含的陈述:浏览器一次只能做一件事。这并非绝对正确,但可以作为一种合理的假设:在任何给定时间,浏览器可以运行 JS、执行布局或绘制,但一次只能执行其中一项。您可以在开发者工具的时间轴视图中验证这一点。此规则的一个例外情况是 Chrome for Android 上的 CSS 动画(桌面版 Chrome 很快也会支持,但目前还不支持)。
尽可能使用 CSS 动画,这样既可以简化应用,又可以让动画流畅运行,即使在 JavaScript 运行时也是如此。
// see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for info on rAF polyfills
rAF = window.requestAnimationFrame;
var degrees = 0;
function update(timestamp) {
document.querySelector('#foo').style.webkitTransform = "rotate(" + degrees + "deg)";
console.log('updated to degrees ' + degrees);
degrees = degrees + 1;
rAF(update);
}
rAF(update);
如果您点击该按钮,JavaScript 会运行 180 毫秒,从而导致卡顿。但如果改用 CSS 动画来驱动该动画,则不会再出现卡顿。
(请记住,在撰写本文时,CSS 动画仅在 Android 版 Chrome 上不会出现卡顿,在桌面版 Chrome 上则会出现卡顿。)
/* tools like Modernizr (http://modernizr.com/) can help with CSS polyfills */
#foo {
+animation-duration: 3s;
+animation-timing-function: linear;
+animation-animation-iteration-count: infinite;
+animation-animation-name: rotate;
}
@+keyframes: rotate; {
from {
+transform: rotate(0deg);
}
to {
+transform: rotate(360deg);
}
}
如需详细了解如何使用 CSS 动画,请参阅 MDN 上的这篇文章等文章。
总结
简而言之:
- 在动画制作中,为每次屏幕刷新生成帧至关重要。 Vsync 动画对应用的使用体验有很大的积极影响。
- 在 Chrome 和其他现代浏览器中实现垂直同步动画的最佳方式是使用 CSS 动画。当您需要比 CSS 动画提供的灵活性更高的灵活性时,最佳技术是基于 requestAnimationFrame 的动画。
- 为确保 rAF 动画正常运行,请确保其他事件处理脚本不会妨碍 rAF 回调运行,并使 rAF 回调保持简短(<15 毫秒)。
最后,vsync 动画不仅适用于简单的界面动画,还适用于 Canvas2D 动画、WebGL 动画,甚至静态页面上的滚动。在本系列的下一篇文章中,我们将结合这些概念深入探讨滚动性能。
祝您动画制作顺利!