Jank Busting 以获得更好的渲染性能
您希望您的 Web 应用程序在执行动画、过渡和其他小的 UI 效果时感觉响应迅速且流畅。 确保这些效果器没有卡顿可能意味着“原生”感觉与笨重、未经修饰的感觉之间的区别。
这是浏览器渲染性能优化系列文章的第一篇。 首先,我们将介绍为什么流畅的动画很困难,实现它需要做些什么,以及一些简单的最佳实践。 其中许多想法最初是在“Jank Busters”中提出的,这是 Nat Duca 和我在 Google I/O 演讲(视频 、 幻灯片)上发表的演讲。 今年
引入垂直同步
PC 游戏玩家可能熟悉这个术语,但在网络上并不常见:什么是 垂直同步 ?
考虑一下您手机的显示屏:它定期刷新,通常(但不总是!)大约每秒 60 次。 垂直同步(或垂直同步)是指仅在屏幕刷新之间生成新帧的做法。 您可能会认为这是将数据写入屏幕缓冲区的进程与读取该数据以将其显示在显示器上的进程之间的竞争条件。 我们希望缓冲的帧内容在这些刷新之间发生变化,而不是在刷新期间; 否则显示器会显示一帧的一半和另一帧的一半,导致“ 撕裂 ”。
为了获得流畅的动画效果,您需要在每次屏幕刷新时准备好新的帧。 这有两个重要的含义:帧时间(即帧需要准备的时间)和帧预算(即浏览器必须在多长时间内生成一个帧)。 您只有屏幕刷新之间的时间来完成一个帧(在 60Hz 屏幕上约为 16 毫秒),并且您希望在最后一帧显示在屏幕上后立即开始生成下一帧。
时间就是一切:requestAnimationFrame
许多网络开发人员使用 setInterval
或者 setTimeout
每 16 毫秒创建一次动画。 由于多种原因(我们将在一分钟内讨论更多),这是一个问题,但特别值得关注的是:
- 来自 JavaScript 的计时器分辨率仅在几毫秒的数量级
- 不同的设备有不同的刷新率
回想一下上面提到的帧计时问题:您需要一个完整的动画帧,完成任何 JavaScript、DOM 操作、布局、绘画等,以便在下一次屏幕刷新发生之前做好准备。 较低的计时器分辨率可能会导致难以在下一次屏幕刷新之前完成动画帧,但屏幕刷新率的变化使得固定计时器无法实现。 无论计时器间隔是多少,您都会慢慢偏离帧的计时窗口并最终丢掉一帧。 即使计时器以毫秒精度触发也会发生这种情况,但它不会(正如开发人员发现的那样 ) ——计时器分辨率会根据机器是使用电池还是插入电源而有所不同,可能会受到后台选项卡占用资源的影响,等。即使这种情况很少见(例如,每 16 帧一次,因为您偏离了一毫秒),您也会注意到:您每秒会丢掉几帧。 您还将进行生成永远不会显示的帧的工作,这会浪费您本可以用于在应用程序中做其他事情的功率和 CPU 时间。
This little utility (by Nat Duca) 说明了这一点。
不同的显示器有不同的刷新率:60Hz 很常见,但有些手机是 59Hz,有些笔记本电脑在低功耗模式下降至 50Hz,有些台式机显示器是 70Hz。
在讨论渲染性能时,我们倾向于关注每秒帧数 (FPS),但方差可能是一个更大的问题。 我们的眼睛会注意到动画中微小的、不规则的障碍,这是一个时间不佳的动画可能产生的。
获得正确定时的动画帧的方法是 requestAnimationFrame
. 使用此 API 时,您是在向浏览器请求动画帧。 当浏览器即将生成新框架时,将调用您的回调。 无论刷新率是多少,都会发生这种情况。
requestAnimationFrame
还有其他不错的属性:
- 后台选项卡中的动画会暂停,从而节省系统资源和电池寿命。
- 如果系统无法以屏幕的刷新率处理渲染,它可以限制动画并降低产生回调的频率(例如,在 60Hz 屏幕上每秒 30 次)。 虽然这会将帧速率降低一半,但它可以保持动画的一致性——如上所述,我们的眼睛更倾向于变化而不是帧速率。 稳定的 30Hz 看起来比每秒丢失几帧的 60Hz 更好。
requestAnimationFrame
已经到处讨论了,所以请参阅 creative JS 的这篇 文章以获取更多信息,但这是平滑动画的重要的第一步。
框架预算
由于我们希望在每次屏幕刷新时都准备好一个新框架,因此只有在刷新之间的时间才能完成创建新框架的所有工作。 在 60Hz 的显示器上,这意味着我们有大约 16 毫秒的时间来运行所有 JavaScript、执行布局、绘制以及浏览器必须执行的任何其他操作以获取框架。 这意味着如果你的 JavaScript requestAnimationFrame
回调运行时间超过 16 毫秒,您没有任何希望及时为垂直同步生成帧!
16ms 不是很多时间。 幸运的是,如果您在 requestAnimationFrame 回调期间超出了帧预算,Chrome 的开发人员工具可以帮助您进行追踪。
考虑 这个示例 ,该示例改编自 Paul Irish 关于使用开发工具查找 动画中的性能问题的精彩视频。
打开 Dev Tools 时间轴并录制这个正在运行的动画,很快就会发现我们在制作动画时超出了预算。 在时间轴中切换到“帧”并查看:
这些 requestAnimationFrame (rAF) 回调花费的时间超过 200 毫秒。 这对于每 16 毫秒勾出一个帧来说太长了一个数量级! 打开其中一个长 rAF 回调揭示了内部发生的事情:在这种情况下,有很多布局。
Paul 的视频更详细地介绍了重新布局的具体原因(正在阅读 scrollTop
) 以及如何避免它。 但这里的要点是,您可以深入了解回调并调查是什么导致了这么长时间。
一旦我们 修复了这个例子 ,框架看起来又漂亮又短:
注意 16 毫秒的帧时间。 框架中的空白区域是您必须做更多工作的余量(或让浏览器完成它需要在后台完成的工作)。 那个空白是一件好事。
Jank 的其他来源
尝试运行 JavaScript 驱动的动画时,最大的麻烦是其他东西会妨碍 rAF 回调,甚至根本无法运行。 即使您的 rAF 回调很精简并且仅在几毫秒内运行,其他活动(例如处理刚进入的 XHR、运行输入事件处理程序或在计时器上运行计划更新)也可能突然进入并运行任何时间段时间不屈服。 在移动设备上,有时处理这些事件可能需要数百毫秒,在此期间您的动画将完全停止。 我们称这些动画故障为 jank 。
没有避免这些情况的灵丹妙药,但有一些架构最佳实践可以让您为成功做好准备:
- 不要在输入处理程序中做很多处理! 在滚动处理程序期间执行大量 JS 或尝试重新排列整个页面是导致严重卡顿的常见原因。
- 将尽可能多的处理(阅读:任何需要很长时间运行的东西)推送到您的 rAF 回调或 Web Worker 中。
- 如果您将工作推入 rAF 回调,请尝试将其分块,这样您每帧只处理一点点或将其延迟到重要动画结束后——这样您就可以继续运行简短的 rAF 回调并流畅地制作动画.
有关如何将处理推送到 requestAnimationFrame 回调而不是输入处理程序的精彩教程,请参阅 Paul Lewis 的文章 使用 requestAnimationFrame 制作更精简、更精简、更快的动画 。
CSS 动画
在您的事件和 rAF 回调中,有什么比轻量级 JS 更好的? 没有JS。
之前我们说过,没有避免中断 rAF 回调的灵丹妙药,但您可以使用 CSS 动画来完全避免对它们的需要。 特别是在 Android 版 Chrome 上(其他浏览器正在开发类似功能),CSS 动画具有非常理想的属性,即使 JavaScript 正在运行,浏览器也可以经常运行它们。
上面关于卡顿的部分有一个隐含的声明:浏览器一次只能做一件事。 这并不完全正确,但这是一个很好的工作假设:在任何给定时间,浏览器都可以运行 JS、执行布局或绘画,但一次只能运行一个。 这可以在开发工具的时间线视图中验证。 这条规则的一个例外是 Android 版 Chrome 上的 CSS 动画(很快桌面版 Chrome,虽然还没有)。
如果可能,使用 CSS 动画既可以简化您的应用程序,也可以让动画流畅地运行,即使在 JavaScript 运行时也是如此。
例如,考虑 这个由 rAF 驱动的旋转 Chrome 徽标示例 :
// 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 上的此类 文章。
Wrapup
它的简称是:
- 制作动画时,为每次屏幕刷新生成帧很重要。 Vsync 的动画对应用程序的感觉产生了巨大的积极影响。
- 在 Chrome 和其他现代浏览器中获得垂直同步动画的最佳方法是使用 CSS 动画。 当您需要比 CSS 动画提供更多的灵活性时,最好的技术是基于 requestAnimationFrame 的动画。
- 为了保持 rAF 动画健康和快乐,请确保其他事件处理程序不会妨碍您的 rAF 回调运行,并保持 rAF 回调较短(<15 毫秒)。
最后,vsync 动画不仅适用于简单的 UI 动画——它适用于 Canvas2D 动画、WebGL 动画,甚至静态页面上的滚动。 在本系列的下一篇文章中,我们将牢记这些概念来深入研究滚动性能。
有关在 Web 上查找卡顿的更多信息,请参阅 jankfree.com 。
参考链接
- jankfree.com for Jank Busters I/O talk, slides, and other info
- Using requestAnimationFrame for drag events
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论