[译] 高性能动画(为什么使用 translate() 比 pos: abs top/left 移动元素更好)

2,552 阅读7分钟

原文链接:High Performance Animations,by Paul Lewis & Paul Irish

Photo by Ricardo Rocha on Unplash

在现代浏览器中,可以非常低成本地执行下列四类动画:定位(position)、缩放(scale)、旋转(rotate)和透明度变换(opacity)。如果你是做其他动画类型的话,需要自担风险,因为很有可能达不到 60 帧的流畅画面效果。

四类动画在浏览器中可以非常低成本执行的动画
定位 transform: translate(npx, npx);
缩放 transform: scale(n);
旋转 transform: rotate(ndeg);
透明度变换 opacity: 0...1

下面是针对同一个动画,分别使用 top/lefttranslate() 的对比效果。

GIF.gif

我们能明显感觉到右面的动画效果(translate 实现)更加顺滑。

从 DOM 到像素

当你使用 Chrome DevTools Timeline 面板时,会看到类似下面的界面:

浏览器所经历的过程非常简单:计算应用到元素的样式(Recalculate Style)、生成元素的几何形状及定位信息(Layout)、将每个元素的像素填充到 图层 中(Paint Setup and Paint)、在屏幕中绘制图层(Composite Layers)。

为了实现平滑的动画,最好的方式是改变只会影响 组合图层 这一阶段的属性—— transformopacity时间轴瀑布的从起点开始,越长,就表示浏览器需要做越多的工作,才能将像素显示到屏幕上。

这个建议基本上是跨浏览器兼容的。Chrome、Firefox、Safari 和 Opera 所有的硬件都可以对 transformopacity 变换做加速。不幸的是,目前还不清楚 Internet Explorer 10+ 使用什么标准来确定硬件加速是否合适,但希望当 IE11 中的 F12 工具发布时,这一点会变得更清楚。

修改布局属性(Layout Properties)

当你在修改元素的时候,浏览器可能会重新布局,所有因修改受到影响的元素的几何形状(定位和尺寸)都要再计算一遍。有时,你修改了一个元素,可能也会导致其他元素的重新计算。例如,如果修改了 <html> 元素的 width,那么它的所有后代元素都会受到影响。由于元素溢出或相互作用的影响,发生在 DOM 树内的修改有时会导致布局计算一直影响到顶部。

可见的元素树越大,执行布局计算所需的时间就越长,因此必须尽量避免使用会触发布局重新计算的属性。

你是否会把应用程序的一些状态存储元素中,或者是元素会使用类名中?当这些元素被修改时,浏览器可能会被迫重新进行样式计算和布局。要注意在程序中会触发布局计算的属性;它们可能并没有产生动画效果,但开销却是很高的!

这里列举了 最为普遍使用的、修改后会影响布局的 CSS 属性列表

会影响布局的一些样式

width height
padding margin
display
border-width
border top
position font-size
float text-align
overflow-y font-weight
overflow left
font-family line-height
vertical-align right
clear white-space
bottom min-height

Source: goo.gl/lPVJY6

修改绘制属性(Paint Properties)

改变一个元素也可能触发绘制,现代浏览器中的大多数绘图都是在软件的 rasterizers 中完成的。根据应用中元素是的分组方式,除了修改的元素外,同一个图层里其他元素可能也需要绘制。

如果你对图层还感觉陌生的话,可以阅读 Tom Wiltzius 写的 这篇文章

会影响绘图的一些样式

color border-style
visibility background
text-decoration background-image
background-position
background-repeat
outline-color outline
outline-style border-raduis
outline-width box-shadow
background-size

Source: goo.gl/lPVJY6
_
如果使用上面的任一属性设置了动画,则被修改的元素将会重新绘制,并将它们所属的图层上传到 GPU。在移动设备上,这特别昂贵,因为 CPU 的功能远不如台式机上的强大,这意味着绘画工作需要更长的时间。 而且 CPU 和GPU 之间的带宽(bandwidth)是有限的,因此纹理(texture)上传需要很长时间。

修改组合属性(Composite Properties)

有一个 CSS 属性,你感觉可能会导致绘制,但却不是的:opacity。GPU 可以在合成期间简单地通过较低的 alpha 值来绘制元素纹理,来处理对不透明度的更改。但是,要使该功能起作用,该元素必须 是图层里唯一的元素。如果还有其他元素一起组合的话,则在 GPU 中对不透明度的更改也会(错误地)使它们褪色。

在 Blink 和 WebKit 浏览器中,会为具有 CSS 过渡或不透明度动画的元素创建新的图层,但是许多开发人员使用translateZ(0)translate3d(0, 0, 0) 手动强制创建图层。

强制创建图层可确保在动画开始时立即绘制图层并准备就绪(创建和绘制图层是一项非常重要的操作,可能会延迟动画的开始),并且没有由于抗锯齿的变化导致外观上的突然变化。不过,应该谨慎地进行使用它,过多的图层可能会导致出现 jank

改变一个元素的 transform 可以归结为修改位置、旋转或缩放。我们经常会使用 top 和 left 值设置定位动画,问题如上所示,lefttop 属性的改变都会触发了布局操作,开销高。更好的解决方案是在元素上使用 translate,因为它不会触发重新布局。

在 Chrome Canary 和 Safari 中,你也可以设置滤镜动画效果,这些滤镜是在主线程中处理的,通常会被加速处理表现得也很好。但是,由于 Internet Explorer 或 Firefox 中还不支持,大家要谨慎使用。

命令式动画和声明式动画

开发者有时会犹豫这个动画效果是用 JavaScript(命令式)实现好呢,还是用 CSS(声明式)实现呢?它们都有各自的优缺点,我们来看一下:

命令式

命令式动画的主要优点也恰恰是它的主要缺点:JavaScript 是在浏览器的主线程上运行的。主线程已经在忙于其他JavaScript、样式计算、布局和绘制了。因此,通常会存在线程争用。这大大增加了丢失动画帧的机会,这是我们最不想要的。

使用 JavaScript 执行动画确实可以为我们提供很多控制:开始、暂停、反转、中断和取消都很简单。某些效果,像 视差滚动,也只能用 JavaScript 实现。

声明式

另一种方法是用 CSS 写过渡和动画效果,优点是浏览器可以对此优化。必要的时候,还会创建新的图层,并在主线程之外执行操作,这是一个好处。但很多 CSS 动画缺点是缺乏 JavaScript 动画的表达能力,很难用有意义的方式组合动画,写出的动画也很复杂、容易出错。

展望未来

随着 Web 标准的发展,围绕动画的一些限制将会消失。谷歌的 Ian Vollick 提出了一项建议,研究 允许通过  web workers 来实现动画 的想法,而且动画本身不会触发布局或样式的重新计算。

对于声明式的动画方法感兴趣的人,可以看看 Web 动画规范Jake Archibald 已经详细介绍过了

总结

好的动画是一个 Web 体验的核心。你应该尽量避免使用会触发布局或绘制的动画属性,这两者开销挺大的,可能导致跳帧(skipped frames)的发生。声明式动画比命令式动画更好,因为浏览器可以提前做优化。

如今,transform 是用来制作动画的最佳属性,因为 GPU 可以辅助这项繁重的工作。因此,可以将动画的限制在以下几种类型。

  • opacity
  • translate
  • rotate
  • scale

在未来,可能会有新的动画方式,让你尽可能像使用 JavaScript 那样的表达,不用主线程成本;或可以没有限制的优化 CSS 动画,但在这之前,请收下这里介绍的,能让你收获顺滑动画体验的技巧吧。

其他资源

(正文完)


广告时间(长期有效)

我有一位好朋友开了一间猫舍,在此帮她宣传一下。现在猫舍里养的都是布偶猫。如果你也是个爱猫人士并且有需要的话,不妨扫一扫她的【闲鱼】二维码。不买也不要紧,看看也行。

(完)