用 Element.animate 实现“无视一切”的动画

Firefox 48 中,我们发布了 <a href="https://mdn.org.cn/docs/Web/API/Element/animate" target="_blank"><b>Element.animate()</b></a> API — 一种新的使用 JavaScript 编程方式来为 DOM 元素进行动画的方法。让我们停顿一下,您可能会说“没什么大不了的”,或者“有什么好大惊小怪的?”毕竟,市面上已经有很多可以选择的动画库了。在这篇文章中,我想解释一下是什么让 Element.animate() 变得特殊。

什么是性能

Element.animate() 是我们发布的 Web Animations API 的一部分,尽管 API 本身有很多很棒的功能,比如更好的动画同步、组合和变形动画,扩展 CSS 动画等,但 Element.animate() 最大的优势在于性能。在某些情况下, Element.animate() 使您能够创建无卡顿的动画,而这些动画仅用 JavaScript 是无法实现的。

不相信?请看一下下面的演示,它比较了左侧的最佳 JavaScript 动画和右侧的 Element.animate(),同时定期运行一些耗时的 JavaScript 来模拟浏览器繁忙时的性能。

常规 JavaScript 动画与 Element.animate() 的性能对比要亲眼看看,请尝试在最新版本的 Firefox 或 Chrome 中 加载演示。然后,您可以查看我们一直在构建的 完整演示集

谈到动画性能,很多信息都是相互矛盾的。例如,您可能听说过一些惊人的(但不真实)的说法,比如“CSS 动画在 GPU 上运行”,然后点头说“嗯,我不知道这是什么意思,但这听起来很快”。因此,为了理解是什么让 Element.animate() 变得快速以及如何充分利用它,让我们深入了解一下是什么让动画变得缓慢。

动画就像洋葱(或者蛋糕。或者圣代。)

为了让动画看起来流畅,我们希望动画每一帧所需的全部更新都在大约 16 毫秒内完成。这是因为浏览器尝试以与它们绘制的显示器的刷新率相同的速率更新屏幕,通常为 60Hz。

在每一帧中,浏览器通常会执行两个需要时间的事:计算页面上元素的布局,以及绘制这些元素。现在,希望您已经听到过这样的建议:“不要动画化更新布局的属性”。我在这里充满希望——当前使用指标表明 Web 开发人员明智地选择动画化诸如 transformopacity 之类的属性,这些属性不会影响布局,只要可以,他们就会这样做。 (color 是另一个不需要重新计算布局的属性的示例,但我们将在稍后看到为什么不透明度更好)。

如果我们可以在每一帧动画中避免执行布局计算,那么就只剩下绘制元素了。事实证明,编程不是唯一一个懒惰是美德的行业——实际上,动画师很早就想出来,他们可以通过创建部分透明的 cels 来避免绘制大量非常相似的帧,将 cels 移动到背景上,并在途中截取快照。

Example of cels used to create animation frames

使用 cels 创建动画帧的示例。
(当然,并非所有人都使用花哨的 cels;有些人只是剪掉圣诞贺卡。)

几年前,浏览器也发现了这种“拉取 cel”技巧。如今,如果浏览器看到某个元素正在移动而没有影响布局,它将绘制两个单独的图层:背景和移动元素。在每一帧动画中,它只需要重新定位这些图层并截取快照,而无需重新绘制任何内容。事实证明,这种截取快照(更专业地称为合成)是 GPU 非常擅长的。更重要的是,当它们合成时,GPU 可以应用 3D 变换和不透明度淡入淡出,而无需浏览器重新绘制任何内容。因此,如果您正在为元素的变换或不透明度设置动画,浏览器可以将大部分工作留给 GPU,并且更有可能在 16 毫秒内完成。

提示:如果您熟悉 Firefox 的 Paint Flashing Tool 或 Chrome 的 Paint Rectangles ,您会注意到什么时候正在使用图层,因为您会看到,即使元素正在动画化,也没有任何东西被绘制!要查看实际的图层,您可以在 Firefox 的 about:config 页面中将 layers.draw-borders 设置为 true,或者在 Chrome 的渲染选项卡中选择“显示图层边界”。

你得到一个图层,你也得到一个图层,每个人都得到一个图层!

信息很明确——图层很棒,您期待着浏览器一定会充分利用这项了不起的发明,将您页面的内容排列成 千层蛋糕。不幸的是,图层不是免费的。首先,它们会占用更多内存,因为浏览器必须记住(并绘制)页面上所有本应被其他元素覆盖的部分。此外,如果图层过多,浏览器将花费更多时间来绘制、排列和截取所有这些图层,最终您的动画实际上会变慢!因此,浏览器只会在它非常确定需要时才创建图层——例如,当某个元素的 transformopacity 属性正在动画化时。

但是,有时浏览器直到为时已晚才意识到需要一个图层。例如,如果您为元素的变换属性设置动画,在您应用动画之前,浏览器不会预先知道它应该创建一个图层。当您突然应用动画时,浏览器会感到一阵轻微的恐慌,因为它现在需要将一个图层变成两个图层,并重新绘制这两个图层。这需要时间,最终会中断动画的开始。礼貌的做法(也是确保您的动画平滑、按时开始的最佳方法)是通过在要进行动画的元素上设置 will-change 属性来提前通知浏览器。

例如,假设您有一个按钮,当单击时会切换一个下拉菜单,如下所示。

Example of using will-change to prepare a drop-down menu for animation

实时示例

我们可以向浏览器提示它应该为菜单准备一个图层,如下所示

nav {
  transition: transform 0.1s;
  transform-origin: 0% 0%;
  will-change: transform;
}
nav[aria-hidden=true] {
  transform: scaleY(0);
}

但您不应该太过分。就像 狼来了的故事 一样,如果您决定 will-change 所有内容,过了一段时间,浏览器就会开始忽略您。最好只对需要绘制时间更长的更大元素应用 will-change,并且仅在需要时应用。Web 控制台 在这里可以帮上忙,它会告诉您何时超出 will-change 预算,如下所示。

Screenshot of the DevTools console showing a will-change over-budget warning.

动画化就像你不在乎一样

现在您已经了解了所有关于图层的知识,我们终于可以进入 Element.animate() 闪耀的部分了。将这些部分组合在一起

  • 通过动画化正确的属性,我们可以避免在每一帧中重新执行布局。
  • 如果我们为 opacitytransform 属性设置动画,通过图层的魔力,我们通常可以避免重新绘制它们。
  • 我们可以使用 will-change 让浏览器知道提前准备图层。

但有一个问题。无论我们多快准备每一帧动画,如果控制浏览器的那部分正在忙于处理其他任务,比如响应事件或运行复杂的脚本,这都没有关系。我们可以在 5 毫秒内完成动画帧,但这没关系,如果浏览器随后花费 50 毫秒来进行 垃圾回收。我们的动画将不会像丝般顺滑,而是会断断续续地进行,破坏运动的幻觉并导致用户血压升高。

但是,如果我们有一个已知的动画不会改变布局,甚至可能不需要重新绘制,那么应该可以让人来处理每一帧调整这些图层。事实证明,浏览器已经有一个专门用于此工作的流程——一个称为合成器的单独线程或流程,专门用于排列和组合图层。我们只需要一种方法来告诉合成器整个动画的故事,并让它开始工作,让主线程(即执行应用程序其他所有操作的浏览器部分)忘记动画,继续生活。

这可以通过使用期待已久的 Element.animate() API 来实现!只需要类似以下代码就可以创建可以在合成器上运行的平滑动画

elem.animate({ transform: [ 'rotate(0deg)', 'rotate(360deg)' ] },
             { duration: 1000, iterations: Infinity });

Screenshot of the animation produced: a rotating foxkeh
实时示例

通过提前说明你要做什么,主线程将感谢你,因为它会很快处理其他所有脚本和事件处理程序。

当然,你可以通过使用 CSS 动画CSS 过渡 来获得相同的效果——实际上,在支持 Web 动画的浏览器中,相同的引擎也用于驱动 CSS 动画和过渡——但对于某些应用程序来说,脚本更适合。

我做对了吗?

你可能已经注意到,要实现无抖动动画,你需要满足几个条件:你需要对 transformopacity 进行动画处理(至少现在是),你需要一个图层,并且你需要提前声明你的动画。那么,你怎么知道自己做对了呢?

Firefox 的 DevTools 中的动画检查器将为在合成器上运行的动画提供一个方便的闪电图标指示器。此外,从 Firefox 49 开始,动画检查器通常可以告诉你你的动画为什么没有被剪辑。

Screenshot showing DevTools Animation inspector reporting why the transform property could not be animated on the compositor.

有关此工具工作原理的更多详细信息,请参阅 相关的 MDN 文章

(注意,结果并不总是正确的——存在一个 已知错误,其中延迟动画有时会告诉你它们没有在合成器上运行,而实际上它们是运行的。如果你怀疑 DevTools 在欺骗你,你总是可以在页面中添加一些长时间运行的 JavaScript,就像本帖中的 第一个示例 一样。如果动画继续快乐地运行,你就知道你做对了——并且,作为奖励,此技术将在任何浏览器中都能正常工作。)

即使你的动画不符合在合成器上运行的条件,使用 Element.animate() 仍然存在性能优势。例如,你可以避免在每一帧上重新解析 CSS 属性,并允许浏览器应用其他一些小技巧,例如忽略当前处于屏幕外的动画,从而延长电池寿命。此外,你将能够使用浏览器在未来想出的任何其他性能技巧(还有更多这样的技巧即将推出)!

结论

随着 Firefox 48 的发布,Element.animate() 已在 Firefox 和 Chrome 的发布版本中实现。此外,还存在一个 polyfill(你可能需要 web-animations.min.js 版本),它将回退到使用 requestAnimationFrame 用于尚不支持 Element.animate() 的浏览器。事实上,如果你使用的是 Polymer 这样的框架,你可能已经在使用它了!

Web 动画 API 中还有很多值得期待的东西,但我们希望你喜欢这一期(演示 等等)!

关于 Brian Birtles

Brian 在日本东京的 Mozilla 工作,负责 Firefox 中的动画和布局。他还编辑 W3C Web 动画和 CSS 动画规范,并且一直在努力为 SVG 做动画,时间太长了。尽管他的个人资料图片显示他是一个海洋爱好者,但他实际上非常喜欢海洋,并梦想着游泳或冲浪去上班。

更多 Brian Birtles 的文章…


9 条评论

  1. Simon T

    你的演示不是误导人了吗?因为他们故意添加了随机延迟。

    2016 年 8 月 3 日 上午 10:50

    1. Brian Birtles

      嗨,Simon!
      最初的演示的目的是模拟一个负载下的浏览器,即你会遇到抖动的情况。长时间运行的 JS 的突发会产生与主线程被占用运行事件处理程序、执行 GC 或任何其他长时间运行的操作相同的效果。由于文章的重点是消除动画抖动,我认为这是一个合理的演示,但也许你建议我可以让它更清楚?

      2016 年 8 月 3 日 下午 16:11

      1. Simon T

        嗨,Brian,

        是的,我觉得一个演示有点极端,没有一点免责声明。在运行演示时,我很惊讶它实际上是卡顿的,这就是为什么我查看了源代码并评论了它。

        不过,完整的演示集合非常不错,我很容易就能在没有人工延迟的情况下看到 JS 和 CSS 之间的性能差异。

        Simon

        2016 年 8 月 3 日 下午 16:36

        1. Brian Birtles

          公平的观点!我已经更新了该演示的介绍,以指出它正在做什么。我很高兴其他演示很有帮助!谢谢 Simon!

          2016 年 8 月 3 日 下午 16:40

  2. Christoph O

    感谢您让我们了解动画 API 的现状。我对这个功能感到非常兴奋,很高兴看到一些部分已经得到了当前浏览器的支持。

    我很好奇在 Android 上比较 Firefox 和 Chrome(最新稳定版本)以及 Windows 机器上,新 API 的性能如何。令我惊讶的是,所有演示(https://mozdevs.github.io/Animation-examples/)在 Chrome 中的运行速度比 Firefox(Android 和 Windows)快得多,而且由于反锯齿,看起来也更漂亮。(观看这两个版本并排运行的录制视频:https://youtu.be/qB6YkClUAUw)机器的频率被限制为 1.2GHz(8GB 内存,酷睿 i7)。

    这里可能是什么问题呢?即使在 Android 上,Firefox 和 Chrome 之间也有很大的差距。我还尝试了 Firefox 的 nightly 版本,但没有改进。

    2016 年 8 月 6 日 上午 03:49

    1. Brian Birtles

      嗨,Cristoph,

      感谢您的评论。在您录制的视频中,动画在主线程上运行,因为它使用的是 transform-style: preserve-3d,而 Gecko 目前在合成器上没有这样做。Mozilla 错误 779598 和错误 1208646 跟踪这项工作,它应该会大幅度提升性能。

      顺便说一句,你可以自己看到这一点,方法是在该页面上打开动画检查器并查看动画。如果你展开任何动画,你将看到 ‘transform’ 有一个点状下划线,当你将鼠标悬停在其上时,它应该会告诉你它没有在合成器上运行,因为使用了 transform-style: preserve-3d。

      2016 年 8 月 8 日 下午 16:28

      1. Christoph O

        嗨,Brian,

        感谢您的澄清。我担心这个演示根本没有使用 GPU,但不知道为什么。继续努力吧!

        此致,
        Christoph

        2016 年 8 月 10 日 上午 00:31

  3. Claire

    嗨,Brian,
    我一直学习成为一名图形设计师。您的教程非常有用且有趣。我很高兴我看到了您的文章。在查看这篇文章后,我更有动力提升自己作为设计师的技能了。
    此致,
    Claire

    2016 年 8 月 8 日 上午 02:25

    1. Brian Birtles

      谢谢 Claire!我很高兴听到你有动力继续提升你的设计技能!

      2016 年 8 月 8 日 下午 16:29

本文的评论已关闭。