实现动画之CSS与JavaScript对比
曾经某个时期,大多数开发者使用 jQuery 给浏览器中的元素添加动画。让这个淡化,让那个扩大,很简单。随着互动的项目越来越复杂,移动设备的大量增加,表现性能变得越来越重要。Flash 被抛弃,有天赋的动画开发者使用 HTML5 去实现过去从未实现的效果。他们需要更好的工具去开发复杂的动画序列并获得最好的性能。jQuery 并不能够做到。浏览器日渐成熟的同时也开始提供了一些解决方案。
最被广泛接受的方案是使用 CSS 动画(以及 Transitions)。几年中,它成为了业内的热门话题,在各种研讨会上,“硬件加速”和“移动端友好”之类的说法总是不绝于耳。基于 JavaScript 的动画总是被当做过时的甚至是“肮脏的”。但是真的是这样吗?
作为一个对动画和表现深深着迷的人,我如饥似渴地投入了 CSS 的怀抱,但当我开始发现了一些大问题后,我却没有深入研究进去。我被震惊了。
这篇文章用于揭示基于 CSS 的动画的一些重大缺陷,这样你可以避免碰到曾经困扰我的问题,同时也教会大家决定何时用 JS 动画以及何时用 CSS 动画。
缺少独立的 scale/rotation/position 控制
对元素的尺寸,旋转以及位置设置动画是非常常见的。在 CSS 里,这些设置都被塞进了transform
属性当中,这样就不能够真正地独立控制它们。例如,你该如何用不同的时间和缓动函数去分别控制元素的rotation
和scale
属性?可能这个元素会不停的震荡,并且你想去旋转它。这只有用 JavaScript 才能实现。
See the Pen Independent Transforms by GreenSock (@GreenSock) on CodePen.
在我看来,这是 CSS 动画一个很显著的问题,但是如果你只需要开发一些简单的动画效果,它们同时触发整个 Transform 状态的话,这也不是什么大问题。
表现性能
大多数比较都会拿 CSS 动画和 jQuery 相比,因为 jQuery 的使用非常普遍(就好像 JavaScript 和 jQuery 是同义词一样)。但是 jQuery 的动画性能很差也是众所周知的。较新的 GSAP 同样是基于 JavaScript 的,但是毫不夸张的说它的性能相比于 jQuery 提升了近 20 倍。因此,JavaScript 动画声名狼藉的部分原因我认为是 jQuery。
使用 CSS 动画的原因中,常常被提起的是“硬件加速”这个概念。听起来很高大上是吧?我们来把它分解成两个部分:
GPU 的使用
GPU 在执行类似控制像素点的移动和应用变换矩阵和透明等方面都做了优化,因此现代浏览器会试着把这方面的任务从 CPU 转交给 GPU 来完成。秘诀在于将应用动画的元素独立出来,建立一个自己的 GPU 层,因为只要一个层被创建,让 GPU 去移动那些像素点并把它们组合起来是很轻松的一件事。不同于以每秒 60 次的速度计算每个像素点的位置,GPU 可以把大量的像素以层的方式储存,然后我们就可以通过像“把那块像素向上移动 10 像素再向下移动 5 像素”这样的方式对像素进行操作了。
注解:GPU 是有图像存储空间限制的,因此将每个元素都转换为一个层是不合适的。一旦 GPU 的存储空间用完了,速度就会急剧降低。
通过 CSS 声明动画能够让浏览器决定哪个元素应该获得 GPU 层,并根据实际情况分配资源。很方便。
**但是你知道你可以用 JavaScript 做到同样的事情吗?**用一个 3D 特性的触发器(比如translate3d()
或者matrix3d()
)来让浏览器为这个元素开辟一个 GPU 层。所以 GPU 加速不仅仅是为 CSS 动画准备的,JavaScript 动画一样可以受益!
另外记住,不是所有的 CSS 属性在 CSS 动画中都能够获得 GPU 的加速。实际上,大多数是不能的。变换(比如 scale,rotation, translation 和 skew)和透明效果是直接受益的。所以不要想当然认为你只要用了 CSS 动画,所有的效果都得到了 GPU 的帮助,那是不对的。
将计算转移给不同的线程
“硬件加速”的另一个方面是使用 CPU 的不同线程来进行和动画相关的计算。再一次的,理论上听起来很美但是它实际上和性能的开销无关,开发者往往会高估它带来的好处。
首先,只有与文档流无关的属性才能真正被移交给另一个线程。所以再一次的,变换和透明度是首要受益者。而转移线程的过程中也是有开销的。在大多数动画中,图像的渲染和文档的展现已经耗费了大多数的处理器资源(这还没有算上中间动画属性本身所耗费的资源),因此转移线程所带来的好处已经微乎其微了。比如,在一个动画中,98%的资源都用来计算图像的渲染和文档流的展现,只有2%的资源用来计算位置、旋转、透明度等属性,即使你将这部分的计算速度加快了十倍,整体带来的速度提升可能也只有1%而已。
性能比较
下面的压力测试创建了一定数量的图像元素(点),并且使用动画让它们从中点沿着随机方向以随机的延迟向边缘飞去,展现了一种飞舞的粒子效果。将粒子的数量提高,观察 jQuery,GSAP 和 Zepto 的比较效果。由于 Zepto 使用了 CSS 来展现所有的动画效果,它的性能应该最好是吗?
See the Pen Speed Test: GSAP vs CSS Transitions (Zepto) vs jQuery by GreenSock (@GreenSock) on CodePen.
结果证实了大多数网上的结论,CSS 动画明显比 jQuery 要快。但是,在我测试的大多数设备和浏览器上,GSAP 的性能甚至比 CSS 还要好(在某些情况下差距还很大,比如在 Microsoft Surface RT 上 GSAP 的速度比 Zepto 创建的 CSS 动画快上 5 倍,并且在 iPad 3 iOS 7 上 GSAP 的动画变换也比 CSS Transition要快)。
Animated properties | Better w/JavaScript | Better w/CSS |
---|---|---|
top, left, width, height | Windows Surface RT, iPhone 5s (iOS7), iPad 3 (iOS 6), iPad 3 (iOS7), Samsung Galaxy Tab 2, Chrome, Firefox, Safari, Opera, Kindle Fire HD, IE11 | (none) |
transforms (translate/scale) | Windows Surface RT, iPhone 5s (iOS7), iPad 3 (iOS7), Samsung Galaxy Tab 2, Firefox, Opera, IE11 | iPad 3 (iOS6), Safari, Chrome |
到底有多快?在测试的最初版本中,有每秒渲染帧数这一量化指标,但是很快就发现并没有一个准确的方式跨浏览器去测量 FPS 的值,尤其是 CSS 动画。并且特定的浏览器会得到令人误解的数字,所以我把它移除了。你可以进行间接的测量,比如逐渐提高粒子数量,切换不同的动画引擎,观察动画的渲染质量(移动平滑稳定,粒子分散度高等等)。毕竟,我们的目标是让动画看起来好看。
一些有趣的事情:
- 当应用动画的属性是会影响文档流的属性,比如 top/left/width/height 值时,JavaScript 的性能会更好(是 GSAP,不是 jQuery)。
- 某些设备看起来对 Transiforms 变换做了优化,而另一些处理 top/left/width/height 的变化显得更好。尤其显著的是,老的 iOS6 在处理 CSS 动画变换时表现更好,而更新的 iOS7 系统在这方面退步了,而现在更是明显慢了。
- 在 CSS 动画刚刚启动的时候都会有一个明显的延迟,这是由于浏览器要计算层并把数据传输至 GPU,基于 JavaScript 的动画同样存在这样的问题。所以说“GPU加速”也是有自己的开销的。(译者注:这个问题可由新的 CSS 属性
will-change
来解决,相关文档) - 在压力比较大的时候,CSS 变换创建的粒子会以一种类似带状或环状的形式喷出(这似乎是同步时序的问题,是由不同线程处理而造成的)。
- 在一些浏览器中(比如 Chrome),当动画中的粒子数非常多的时候,文字的透明渐隐就会完全消失,但这样的情况只存在于 CSS 动画中!
尽管经过优化的 JavaScript 动画经常和 CSS 动画一样快,甚至更快,但是 CSS 处理 3D 变换的速度还是更快,但这还跟当今浏览器处理 16 元素的矩阵的方式有关(强制将数字转换为连接的字符串,再转换为数字)。好在这种情况将会改变,在大多数实际的项目中,你并不会感觉到这种区别。
我鼓励你在自己的项目中通过自己的测试去找到性能最高的实现动画的方式。不要相信 CSS 动画性能一定高的说法,也不要让上文的测试影响你自己的应用中的结果。一定要自己测试,测试,再测试。
运行时的控制和事件
一些浏览器允许你在 CSS keyframes 动画中暂停,但最多也就是这样了。你不可能在动画中寻找一个特定的时间点,不可能在半路反转动画,不可能变换时间尺度,不可能在特定的位置添加回调函数或是将他们绑定在一大堆回放事件上[?]。JavaScript 提供了很棒的控制方法,请看下面的例子:
See the Pen Impossible with CSS: controls by GreenSock (@GreenSock) on CodePen.
现代动画大多看重交互性,因此从多种起始变量通过动画变换为结束状态就显得尤其有用(例如可能是基于用户点击鼠标的位置),或者改变正在运动中的元素,提前声明式的 CSS 动画就做不到这一点。
工作流
对用两个状态之间的简单切换(比如翻转或是展开菜单),CSS 变换是很好用的。对于连续变换状态的动画,你需要使用 CSS keyframes 动画来实现,它迫使你通过百分比的方式来声明动画,就像这样:
@keyframes myAnimation { 0% { opacity: 0; transform: translate(0, 0); } 30% { opacity: 1; transform: translate(0, 0); } 60% { transform: translate(100px, 0); } 100% { transform: translate(100px, 100px); } } #box { animation: myAnimation 2.75s; }
但是动画进行的时候,你难道不觉得通过时间表示比使用百分比更好吗?就像“用 1 秒钟降低透明度,再用 0.75 秒向右滑动,再过 1 秒后向下掉落”。如果你用了几个小时通过百分比的方式实现后,客户又要求你将中间的步骤用时增加 3 秒呢?你需要重新计算所有的百分比了!
创建动画的过程中常常需要进行很多的试验,尤其是针对时间和缓动方式,这是seek()
方法能派上用场的地方。想象你创建了一个 60 秒长度的动画然后需要对最后 5 秒进行处理,为了看到编辑后的效果,你每次都要先等上 55 秒钟的时间才行。你可以使用这个方法直接跳过前面的时间,最后再移除这个方法,这样可以节省大量的时间。
现在创建基于 canvas 的对象以及第三方库对象的动画的场景越来越多,而 CSS 动画又只能以 DOM 元素为目标。也就是说即使你花了大量的时间研究 CSS 动画,在那样的项目中也不会有用,最后你还是不得不更换工具。
以下是一些其他的 CSS 动画不能提供的工作流程相关的便利:
- 相对值。比如“再旋转 30 度”或是“将元素由动画开始的地方再向下移动 100 像素”。
- 嵌套。想象把一个动画嵌套在另一个同样可以被嵌套的动画之中,等等。设想控制主动画的时候所有的动画都能同步。这样的结构能促进生成模块化的代码,利于生产和维护。
- 进程报告。特定的动画结束了吗?如果没有,它现在处于整个过程中的什么位置?
- 特定删除。有时候,只移除一个元素上所有影响其尺寸(或任何你认为的属性)的动画同时允许其他的动画进行是非常有用的。
- 代码简洁。即使你不添加多余带前缀的属性,CSS keyframes 动画的代码也会非常冗长。任何想用 CSS 实现稍微复杂一点动画的人都能证实 CSS 代码最后会变得非常笨重。实际上,实现动画的 CSS 代码的体积可能都会超过 JavaScript 库的体积(在很多动画中它还能被缓存和重用)。
有限的效果
以下的任何效果你都无法通过 CSS 实现:
- 沿着曲线运动(比如贝塞尔曲线)
- 使用有趣的缓动效果,比如有弹性的,或是弹跳,或是粗糙的效果。虽然有
cubic-bezier()
这个选项,但它只允许两个控制点,因此功能也十分有限。 - 在 CSS keyframes 动画中针对不同的属性使用不同的缓动效果。缓动会应用到整个 CSS keyframes 上。
- 基于物理的动作。例如,有冲击效果的闪烁,或是恢复状态,就像这个Demo中的一样。
- 滚动位置的动画。
- 指定方向的旋转。比如沿最短方向旋转270度,或顺时针和逆时针。
- 元素属性的动画
兼容性
基于 CSS 的动画在 IE9 及之前版本的浏览器中都无效,我们大多都讨厌适配老版本的浏览器(尤其是 IE),但现实是我们的一些客户会要求那样的支持。
对许多浏览器来说,前缀是需要的,不过你可以利用预编译工具来避免手动书写它们。
结论
CSS 动画不好吗,当然不是。实际上,在实现一些简单的状态变换(比如翻转)并不要求对老版本浏览器兼容时 CSS 动画是非常方便的。3D 变换经常能有非常好的表现(iOS7是个例外)。而且对于喜欢把所有动画实现都放在 CSS 层的开发者来说 CSS 动画非常有吸引力。但是,基于 JavaScript 的动画灵活性更高,能更好的实现复杂的动画和大量的交互。并且和你听说的不一样,它能有和 CSS 动画一样,甚至更好的性能。
当和jQuery.animate()
进行比较时,我能够理解为什么 CSS 动画会那么受欢迎了。谁不想要 10 倍的性能提升呢。但是很快就不用在 jQuery 和 CSS 中进行选择了,基于 JavaScript 的工具 GSAP 提供了全新的可能性,也消除了性能的差距。
这篇文章不是要说 GSAP 或是任何其他的库;而是要说 JavaScript 动画不应该背上坏名声。实际上,JavaScript 是搭建健壮的,灵活的动画系统的唯一选择。另外,我也想让大家看到 CSS 动画中令人失望的部分,这样,你可以对动画实现的方式进行更科学的选择。
Web Animation 标准能解决问题吗?
W3C 正在制定一份新的有关解决 CSS 动画和变换缺点的标准,提供更好的控制和更多的功能。从很多方面来说它确实有很大的进步,但它也是有缺陷的(其中的一些方面甚至是不可能的,它们会和现有的合法的 CSS 标准发生冲突,例如,独立的变换控制组件看起来是不可能 的)。但那是另一个话题了,我们可以看看事情会如何发展。毕竟还是有很多聪明人在为这份标准努力的。