站在GPU的角度,优化你的移动端CSS3动画(上篇)
Posted Web上的绘画艺术
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了站在GPU的角度,优化你的移动端CSS3动画(上篇)相关的知识,希望对你有一定的参考价值。
题图是由目前在开发的工具PatternPainter生成。
之前一直在写Three.js的源码剖析文章,一开始我想的是逐个版本地去看,但是发现发展到今天第91个版本的Threejs,中间有大量的工作是推倒重来式的。原因既可能是作者需要扩展Three的功能于是重构了代码、也可能是W3C或者WebGL本身的规范起了变化(目前最新的WebGL规范是第二版了)。所以我转变了思路,直接从最新的版本开始研究,但是这样梳理其脉络就需要很长时间,但 我 不 会 弃 坑。
我的工作还经常要配合前端制作H5动画。目前在公司项目的合作模式中,我会将构思好的动画直接写一个简单的网页Demo,然后用CSS3演示动画方案,这样的好处是,业务、产品同事能够在手机上看到更加还原的方案,前端同事也能直接拿到我的动画参数而不用看着Demo视频不断调节动画参数。
在前几年的年度账单项目里,每次都使用了大量的CSS3动画。有的时候自己做了Demo动画,但是实际开发完事去不同机型上测试的时候会发生卡顿。这个其实也在我的预料中,毕竟移动端的表现也是视机型而定各不相同,但是直到去年看了SmashingMagazine上面的一篇文章,才对CSS3的动画有了深入的理解。
https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/#implicit-compositing
点击下面的原文链接,可以直达这篇文章。
我的这个系列分为两篇,主要部分其实就是对这篇外文的翻译。
CSS3动画大量地运用在目前的H5应用中,但是对于移动设备来说,机型和性能是制约CSS3动画的最主要原因。这篇文章索要探讨的,就是如何在移动设备上优化CSS3动画的显示效率。
现代浏览器使用GPU帮助渲染页面,尤其是动画部分。比如在CSS动画中,使用 transform
属性的动画,要比使用 left
和 top
属性更加流畅。我们还知道,用 transform:translateZ(0)
或 will-change:transform
同样能让动画转到GPU运行。
有时候,动画在网页上运行平滑,但是有时候又会导致浏览器崩溃。这些都是由于什么导致的?我们能够怎样优化它?这是这篇文章所要探讨的。
浏览器的渲染机制
将各个层渲染结合成为一个层,这在浏览器的术语里被称之为「合成(Composite)」。W3C的规范里没有任何关于网页合成渲染的说明,没说过具体怎么把一个元素放进GPU合成,甚至连合成本身这件事都没提过。这只是一种浏览器端的优化,用来提升某些特定任务的表现能力,而且各个浏览器厂商的实现方式也各不相同。
一个页面的所谓「渲染」,简单来说可以认为经历了以下下几个步骤:
其中,脚本语句会做一些视觉上变化的效果,比如往页面里添加一些DOM元素等;计算样式是根据 CSS 选择器,对每个 DOM 元素匹配对应的 CSS 样式。这一步结束之后,就确定了每个 DOM 元素上该应用什么 CSS 样式规则;布局就是具体计算每个 DOM 元素最终在屏幕上显示的大小和位置;渲染绘制这一步,包括绘制文字、颜色、图像、边框和阴影等,也就是一个 DOM 元素所有的可视效果,一般来说,这个绘制过程是在多个不同的层上完成的;最后一步,渲染层合并,指的是浏览器在每个层上完成上一步的绘制过程后,会将所有层按照合理的顺序合并成一个图层,然后显示在屏幕上。对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。
这篇文章所探讨的,正是最后两步,「渲染绘制」和「渲染合成」中的优化。
「渲染合成」的工作机制
比方说一张网页上,有A和B两个元素,它们的 position
都以 absolute
定位,通过 z-index
让它们一前一后有所重叠,如下图所示:
然后让A元素以一个keyframes动画做来回的振动,如下图所示:
在这个简单的例子中,动画的每一帧,浏览器都要重新计算DOM元素的几何特征(或者叫 重算,reflow),然后重新渲染图像(也叫 重绘,repaint ),然后把这些渲染要求派给GPU来显示在屏幕上。由于重绘相对来说是比较吃计算的,所以现代浏览器通常只重绘页面上发生变化的区域。
大多数时候浏览器能够快速重绘,但如果一个页面充满了复杂交错的布局、层层重叠的动画,重计算和重绘制这两步就格外消耗计算效能。
如果我们可以分开绘制两部分:一部分是运动着的A元素,另一部分是除了A以外的页面所有其他元素,然后只让互相之间有相对位移的部分动起来,这就显然更有效率一些。换句话说,对缓存好的元素进行合成计算要更快一些,因为这正是GPU擅长的地方:用亚像素精度(Subpixel precision)来构建图像,让动画看起来更加流畅顺滑。
接下来就是如何优化上面的动画。但是优化之前,CSS动画需要满足以下几条条件:
动画不会影响文档的正常流式布局
动画不依赖于文档的流式布局
动画不会引起重绘
你可能会想, top
、 left
属性,以及诸如 position
, absolute
和 fixed
这样的属性,并不依赖于元素所处的文档环境,但并非如此。比如, left
属性也许接收的是一个依赖于父元素的百分比值,或者, em
、 vh
这样的依赖环境的值等等。说到这,我们发现,css中只有 transform
和 opacity
满足上述的条件。
那么我们用 tranform
代替 left
再来试试动画:
<style>
#a, #b {
position: absolute;
}
#a {
left: 10px;
top: 10px;
z-index: 2;
animation: move 1s linear;
}
#b {
left: 50px;
top: 50px;
z-index: 1;
}
@keyframes move {
from { transform: translateX(0); }
to { transform: translateX(70px); }
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>
这段代码中我们像公告一样声明了动画的起始位置、结束位置、持续时间等等。这等于在一切开始之前告诉了浏览器,哪一部分CSS属性会改变,浏览器看了一下后发现没有什么属性会产生重算或重绘,于是它放心地进行了这样的一次「优化」操作:将网页划分成两个合成层( compositing layers ),各自渲染成一张图像,然后把它们发给了GPU。
那么,这种优化带来了哪些好处?
我们得到了亚像素级的平滑动画,其赖以执行的单元是专门用来图形优化的,运行起来很快
动画不再绑定在CPU上,就算当前的JS计算量再大,动画依然可以流畅运行
但是这种优化是百利无一害的吗?我们来继续剖析一下:
所谓GPU这个东西,你实际上可以把它当做一台独立的电脑,它有自己独立的处理器、独立的存储单元以及数据处理模块。而浏览器和其他APP、游戏一样,在跟GPU对话时就像在跟一块外部设备交流一样。
可以想想看AJAX。假设你想用一个来访用户的表单数据注册该用户,你不可能仅仅对服务器说拿着这些表单数据,存到数据库里。远程服务器不可能直接获取用户浏览器中的存储数据。你需要把页面上的数据封装成可解析的数据格式(比如JSON)再发给服务器。
合成的过程也一样。GPU相当于服务器,浏览器需要先将数据封装起来再发给它。虽然GPU不像服务器那样在千里之外,但是你也要明白,上网的时候服务器来回传输多了2秒我们尚可忍受,GPU处理时间多了3到5毫秒这个动画可能就没法看了。
那么发给GPU的数据是什么?大部分时候,它是图层合成好的图像( layer images ),可能还带着尺寸、位移、动画参数这样的额外信息。
下面的顺序大致列出了从数据生成到发送给GPU的过程:
把每个划分好的图层绘制成图像
添加信息,如尺寸、偏移、透明度等
添加动画用的shader
将数据传入GPU
每次你使用 transform:translateZ(0)
或 will-change:transform
这些属性时在它内部走的流程都一样。因为重绘非常消耗性能,所以大多数情况下,浏览器不会渐进式重绘。它渲染的区域是之前被新合成的层覆盖的
隐式合成(Implicit Compositing)
回到刚才的例子。之前我们让A元素动,而A元素位于所有页面其他元素的顶上,这就导致了所合成过程中的图层分为两个:一个是A,一个是B以及页面背景。
这一回,我们让B动起来:
元素B应当在一个单独的渲染合成层中,并且最终页面的图像应当是在GPU中合成的。但是A元素应当在B元素之上。
GPU合成模式并不是CSS规范中的一部分,它只是浏览器内在应用的一个优化。我们必须让A出现在B的上方,就像用 z-index
定义过一样。然后浏览器会怎么做?
浏览器会强迫性地为A元素创建一个新的层,当然也会再来一次重绘。
这就是隐式合成,一个或多个非合成元素会出现在合成元素的上面
隐式合成这件事实际上出现的十分频繁。浏览器会出于很多原因将元素送入合成层,比方说下面这些原因:
3D变换:
translate3d
,translateZ
等等<video>
,<canvas>
以及<iframe>
元素通过
Element.animate()
驱动的transform和opacity动画通过CSS transition/animation驱动的transform和opacity动画
使用
position:fixed
固定的元素CSS的
will-change
属性CSS的
filter
属性
隐式合成就是GPU动画的一个缺陷,它会造成非预期的重绘。
存储消耗
GPU不仅仅要讲渲染好的图层发送出去,还要把他们存储起来以备之后的动画使用
一个单独的合成会占用多少存储?我们举个简单的例子。猜一下,存储一个填充成#FF0000的320×240像素的矩形需要用多少内存
一个典型的web程序员会想,这是一个纯色的图片,我把他存成PNG看看多大,应该会小于1KB吧。实际上确实是104bytes。
问题是,PNG也好,JPG或GIF也好,是用来存储和传播图片的。计算机需要把它们以图片格式打开,然后以像素阵列呈现。也就是说,我们例子中的图片的呈现,需要 320×240×3=230,400
比特的内存空间。如果图片有透明度的话,我们就得乘以4,那是307200比特
假设我们有一个10张图片的跑马灯,每张800×600像素。当用户拖拽时,我们能够平滑地在各个图片间过度。为此我们添加了 will-change:transform
属性给每个图片,以便在一切开始之前让每张图片进入合成层,这样一当用户做了交互操作transition过渡就会立即奏效。来,我们计算一下多用了多少存储:800×600×4×10=19MB
一个简单的操作就需要19MB的存储。如果你在做的是一个单页的网站,有大量的动画、效果、高清图片和视觉强化,那么额外的100到200MB内存只是刚开始。如果在算上隐式合成,那么开销几乎能把设备的内存全用上。
对于桌面客户端来说,不算什么,对于移动用户来说,这就够呛了。首先,现在主流的设备都是高清屏,这就要把合成层的大小乘个4到9,再者移动设备的内存本身也不像电脑那么大。iPhone6有1GB的共享内存(就是RAM和VRAM公用),三分之一的内存是被系统占用,还有三分之一是被浏览器和当前页面占用,我们也仅有200到300MB的空间留给GPU。况且iPhone6还是移动设备里不错的了。
你也许会问,是否把PNG图像存入GPU可以减少内存消耗?理论上说可以,但是唯一问题是GPU是一个像素接一个像素地绘制,一整张PNG在GPU手中每个像素都会被绘制一遍又一遍。这种情况下估计动画也就1秒1帧。
所以专门做一个给GPU专用的图像压缩格式没有任何意义,缺乏硬件支持是极大的一个限制。
好处与坏处
现在了解了GPU动画的基本知识,来总结一下优缺点:
优势:
动画又快又平滑,可达到每秒60帧
如果处理地巧妙,动画可以运行在一个单独的线程当中,不会被javascript的沉重计算所牵连
涉及3D变换的动画在GPU这里,「成本」相对CPU的计算低得多
劣势:
如果要把元素推入GPU的合成层,那么需要额外的重绘。有时候这就非常慢,比如对一整张图层的重绘而不是渐进式地重绘一部分。
绘制图层一般需要被转移至GPU,如果图层的数量和尺寸不理想,这种转换过程就会很慢。在中低端机型上,直接导致元素闪烁现象。
每一个需要合成的图层都会消耗额外的内存。在移动设备上,内存就是稀缺资源。超负荷的内存使用往往直接导致浏览器崩溃。
如果你没有事先考虑到隐式合成,那么很大概率上你会碰到:重绘缓慢、内存超载以及浏览器崩溃这些事。
有时候会出现视觉缺陷,比如在Safari中渲染字体,页面内容会消失或扭曲。
所以,使用GPU动画固然有很多优点,但也有不少棘手的问题。最关键的就是过量的重绘和内存使用。
下一篇文章则要谈谈针对这些问题的优化技巧。
以上是关于站在GPU的角度,优化你的移动端CSS3动画(上篇)的主要内容,如果未能解决你的问题,请参考以下文章