消除卡顿,实现更好的渲染性能

Tom Wiltzius
Tom Wiltzius

简介

您希望自己的 Web 应用在执行动画、转场和其他小型界面效果时感觉响应迅速、流畅。确保这些效果不发生卡顿就意味着或笨拙、粗糙的画面

本文是介绍浏览器渲染性能优化的系列文章中的第一篇。首先,我们将介绍流畅动画为什么很难实现、需要执行哪些操作才能实现流畅播放,以及一些简单的最佳实践。其中许多想法最初都是在《Jank Busters》中提出的,我和 Nat Duca 今年在 Google I/O 大会上发表了演讲(视频)。

V-sync 简介

PC 游戏玩家可能熟悉这个术语,但在网络上并不常见:什么是 v-sync

考虑一下手机的显示屏:它会定期刷新,通常(但并不总是!)大约每秒刷新 60 次。垂直同步是指仅在屏幕刷新之间生成新帧的做法。您可以将这种情况看作是,在将数据写入屏幕缓冲区的进程与读取该数据并将其显示在显示屏上之间的竞态条件。我们希望缓冲帧内容在两次刷新之间(而不是在刷新期间)发生变化;否则,显示器会显示两帧的一半,呈现另一帧的一半,从而导致“画面撕裂”。

为了获得流畅的动画效果,每次屏幕刷新时都需要准备好新的帧。这有两个重大影响:帧时间(即帧需要准备就绪的时间)和帧预算(即浏览器生成帧的时长)。您只有两次屏幕刷新之间间隔的时间(在 60Hz 屏幕上,刷新间隔大约为 16 毫秒),并且希望在最后一帧显示在屏幕上时立即开始生成下一帧。

时间即一切:requestAnimationFrame

许多 Web 开发者每 16 毫秒使用 setIntervalsetTimeout 来创建动画。造成这一问题的原因有很多(我们稍后会进行详细讨论),但需要特别注意的是:

  • JavaScript 的计时器解析大约只有几毫秒
  • 不同设备的刷新率不同

回想一下前面提到的帧时间问题:您需要一个已完成的动画帧,并用所有 JavaScript、DOM 操作、布局、绘制等完成,以便在下一次屏幕刷新发生之前做好准备。如果计时器分辨率较低,则可能很难在下一次屏幕刷新之前完成动画帧,但屏幕刷新频率的变化使得使用固定的计时器无法做到这一点。无论计时器的时间间隔是多少,您都会慢慢地离开帧的计时窗口,最后丢掉一个帧。即使计时器以毫秒级精度触发,也会出现这种情况(正如开发者发现的那样),计时器的准确度取决于设备是用电池还是插电等因素来决定,可能会受到后台标签页占用资源等因素的影响。即使这种情况很少见(比方说,每 16 帧的偏差只差 1 毫秒),您也会注意到:丢掉好几帧。此外,您还需要生成一律不显示的帧,这会浪费电量和 CPU 时间,而您可能要在应用中执行其他操作。

不同的显示屏具有不同的刷新率:60Hz 很常见,但有些手机为 59Hz,有些笔记本电脑在低功耗模式下会降至 50Hz,有些桌面设备显示器的刷新率为 70Hz。

在讨论渲染性能时,我们倾向于关注每秒帧数 (FPS),但差异可能是一个更严重的问题。我们的眼睛会注意到动画中会出现微小、不规则的卡顿,而动画时间不当可能会导致动画出现异常。

使用 requestAnimationFrame 可获得正确计时的动画帧。使用此 API 时,您将向浏览器请求动画帧。当浏览器即将生成新帧时,系统会调用您的回调。无论刷新率是多少,都会发生这种情况。

requestAnimationFrame 还具有其他不错的属性:

  • 后台标签页中的动画会暂停,以节省系统资源和电池续航时间。
  • 如果系统无法在屏幕的刷新频率下处理渲染,则可能会限制动画,并降低产生回调的频率(例如,在 60Hz 屏幕上每秒 30 次)。虽然这会将帧速率降低一半,但可以保持动画的一致性 -- 正如上文所述,我们的人眼对变化的适应程度远高于帧速率。稳定的 30Hz 比每秒遗漏几帧的 60Hz 更好。

requestAnimationFrame 已在各处讨论过,因此,请参阅 Creative JS 的这篇博文等文章以了解详情。但要实现流畅动画,这一步至关重要。

帧预算

由于我们希望在每次屏幕刷新时都准备好一个新帧,因此只有在两次刷新之间间隔一段时间才能完成创建新帧的所有工作。在 60Hz 显示屏上,这意味着我们有大约 16 毫秒的时间来运行所有 JavaScript、执行布局、绘制以及浏览器为呈现帧而必须执行的任何其他操作。这意味着,如果 requestAnimationFrame 回调中的 JavaScript 的运行时间超过 16 毫秒,您完全不可能及时生成垂直同步的帧!

16 毫秒可不是很多时间。幸运的是,Chrome 的开发者工具可以帮助您跟踪在 requestAnimationFrame 回调过程中是否超出了帧预算。

打开开发者工具时间轴并快速录制这个动画的实际效果,可以看出我们在制作动画时预算超出了预算。在时间轴中,切换到“帧”看一下:

<ph type="x-smartling-placeholder">
</ph> 布局过多的演示
布局过多的演示

这些 requestAnimationFrame (rAF) 回调花费的时间超过 200 毫秒。这对每 16 毫秒都无法记录一帧,这真是太冗长了!打开其中一个较长的 rAF 回调就能揭示内部发生的情况:在本例中,是大量布局。

Paul 的视频详细介绍了重新布局的具体原因(其内容为 scrollTop),以及如何避免这种情况。但这里的要点是,您可以深入研究回调,调查花这么长时间的操作。

<ph type="x-smartling-placeholder">
</ph> 经过更新的演示,大幅缩减了布局
经过更新的演示,布局大幅缩减

请注意 16 毫秒的帧时间。框架中的空白区域就是您必须执行更多工作(或让浏览器在后台执行其工作)的空间。这块空白区域是件好事。

导致卡顿的其他来源

在尝试运行由 JavaScript 提供支持的动画时造成问题的最大原因 其他因素可能会妨碍 rAF 回调,甚至会阻碍 rAF 回调。 根本无法运行即使您的 rAF 回调较为精简且运行时间短, 其他活动(例如处理刚刚进入的 XHR)、 运行输入事件处理程序,或运行计时器上的计划更新)都可以 突然加入并运行任意一段时间都没有让步。在移动设备上 有时,处理这些事件的设备可能需要数百毫秒的时间, 在此期间,您的动画将完全停滞。我们称之为 动画会遇到卡顿

没有什么能避免这种情况的灵丹妙药,但您可以遵循一些架构最佳实践,为取得成功做好准备:

  • 不要在输入处理程序中进行大量处理!执行大量 JS 或尝试重新排列整个页面,例如滚动处理程序是导致严重卡顿的很常见原因。
  • 将尽可能多的处理作业(读取:任何需要很长时间才能运行的内容)推送到 rAF 回调或 Web Workers
  • 如果您将工作推送到 rAF 回调中,请尝试将工作分块,以便只处理每一帧的少量数据,或者将工作延迟到重要动画结束后再处理 - 这样,您便可以继续运行短时间 rAF 回调,并顺畅地进行动画处理。

有关如何将处理工作推送到 requestAnimationFrame 回调(而不是输入处理程序)的精彩教程,请参阅 Paul Lewis 的文章 Leaner, Meaner, Accelerated Animations with requestAnimationFrame

CSS 动画

在事件和 rAF 回调中,还有什么比轻量级 JS 更好?无 JS。

我们之前说过,没有什么办法可以避免中断 rAF 回调,但您可以使用 CSS 动画来完全避免 rAF 回调的需要。尤其是在 Chrome(Android 版)(以及其他浏览器也在使用类似功能)上,CSS 动画具有非常理想的属性,即使 JavaScript 正在运行,浏览器也经常可以运行它。

上面有关卡顿的部分中有一个隐式语句:浏览器一次只能执行一项操作。这个假设并不严格,但可以有效假设:浏览器在任何时候都可以运行 JS、执行布局或绘制,但每次只能运行一个。可以在开发者工具的“时间轴”视图中对此进行验证。此规则的一个例外情况是 Chrome(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 上的这篇报道等文章。

小结

简而言之:

  1. 在添加动画效果时,为每次屏幕刷新生成帧很重要。 Vsync 的动画对应用的观感产生巨大的积极影响。
  2. 在 Chrome 和其他现代浏览器中获得垂直同步动画的最佳方法是 如何使用 CSS 动画需要比 CSS 动画更大的灵活性 最适合的技术是基于 requestAnimationFrame 的动画。
  3. 为了让 rAF 动画保持良好状态,请确保其他事件处理脚本 不会妨碍 rAF 回调的运行,并保留 rAF 回调 短(<15 毫秒)。

最后,Vsync 的动画不仅适用于简单的界面动画 - 它适用于 Canvas2D 动画、WebGL 动画,甚至在静态页面上滚动。在本系列的下一篇文章中,我们将牢记上述概念,深入探讨滚动性能。

快乐制作动画!

参考