资讯专栏INFORMATION COLUMN

一篇文章说清浏览器解析和CSS(GPU)动画优化

gekylin / 462人阅读

摘要:相信不少人在做移动端动画的时候遇到了卡顿的问题,这篇文章尝试从浏览器渲染的角度一点一点告诉你动画优化的原理及其技巧,作为你工作中优化动画的参考。浏览器渲染提高动画的优化不得不提及浏览器是如何渲染一个页面。

相信不少人在做移动端动画的时候遇到了卡顿的问题,这篇文章尝试从浏览器渲染的角度;一点一点告诉你动画优化的原理及其技巧,作为你工作中优化动画的参考。文末有优化技巧的总结。

因为GPU合成没有官方规范,每个浏览器的问题和解决方式也不同;所以文章内容仅供参考。

浏览器渲染

提高动画的优化不得不提及浏览器是如何渲染一个页面。在从服务器中拿到数据后,浏览器会先做解析三类东西:

解析html,xhtml,svg这三类文档,形成dom树。

解析css,产生css rule tree。

解析js,js会通过api来操作dom tree和css rule tree。

解析完成之后,浏览器引擎会通过dom tree和css rule tree来构建rendering tree:

rendering tree和dom tree并不完全相同,例如:或display:none的东西就不会放在渲染树中。

css rule tree主要是完成匹配,并把css rule附加给rendering tree的每个element。

在渲染树构建完成后,

浏览器会对这些元素进行定位和布局,这一步也叫做reflow或者layout。

浏览器绘制这些元素的样式,颜色,背景,大小及边框等,这一步也叫做repaint。

然后浏览器会将各层的信息发送给GPU,GPU会将各层合成;显示在屏幕上。

渲染优化原理

如上所说,渲染树构建完成后;浏览器要做的步骤:

reflow——》repaint——》composite

reflow和repaint

reflow和repaint都是耗费浏览器性能的操作,这两者尤以reflow为甚;因为每次reflow,浏览器都要重新计算每个元素的形状和位置。

由于reflow和repaint都是非常消耗性能的,我们的浏览器为此做了一些优化。浏览器会将reflow和repaint的操作积攒一批,然后做一次reflow。但是有些时候,你的代码会强制浏览器做多次reflow。例如:

var content = document.getElementById("content");
content.style.width = 700px;
var contentWidth = content.offsetWidth;
content.style.backgound = "red";

以上第三行代码,需要浏览器reflow后;再获取值,所以会导致浏览器多做一次reflow。

下面是一些针对reflow和repaint的最佳实践:

不要一条一条地修改dom的样式,尽量使用className一次修改。

将dom离线后修改

使用documentFragment对象在内存里操作dom。

先把dom节点display:none;(会触发一次reflow)。然后做大量的修改后,再把它显示出来。

clone一个dom节点在内存里,修改之后;与在线的节点相替换。

不要使用table布局,一个小改动会造成整个table的重新布局。

transform和opacity只会引起合成,不会引起布局和重绘。

从上述的最佳实践中你可能发现,动画优化一般都是尽可能地减少reflow、repaint的发生。关于哪些属性会引起reflow、repaint及composite,你可以在这个网站找到https://csstriggers.com/。

composite

在reflow和repaint之后,浏览器会将多个复合层传入GPU;进行合成工作,那么合成是如何工作的呢?

假设我们的页面中有A和B两个元素,它们有absolute和z-index属性;浏览器会重绘它们,然后将图像发送给GPU;然后GPU将会把多个图像合成展示在屏幕上。


A
B

我们将A元素使用left属性,做一个移动动画:


A
B

在这个例子中,对于动画的每一帧;浏览器会计算元素的几何形状,渲染新状态的图像;并把它们发送给GPU。(你没看错,position也会引起浏览器重排的)尽管浏览器做了优化,在repaint时,只会repaint部分区域;但是我们的动画仍然不够流畅。

因为重排和重绘发生在动画的每一帧,一个有效避免reflow和repaint的方式是我们仅仅画两个图像;一个是a元素,一个是b元素及整个页面;我们将这两张图片发送给GPU,然后动画发生的时候;只做两张图片相对对方的平移。也就是说,仅仅合成缓存的图片将会很快;这也是GPU的优势——它能非常快地以亚像素精度地合成图片,并给动画带来平滑的曲线。

为了仅发生composite,我们做动画的css property必须满足以下三个条件:

不影响文档流。

不依赖文档流。

不会造成重绘。

满足以上以上条件的css property只有transform和opacity。你可能以为position也满足以上条件,但事实不是这样,举个例子left属性可以使用百分比的值,依赖于它的offset parent。还有em、vh等其他单位也依赖于他们的环境。

我们使用translate来代替left


A
B

浏览器在动画执行之前就知道动画如何开始和结束,因为浏览器没有看到需要reflow和repaint的操作;浏览器就会画两张图像作为复合层,并将它们传入GPU。

这样做有两个优势:

动画将会非常流畅

动画不在绑定到CPU,即使js执行大量的工作;动画依然流畅。

看起来性能问题好像已经解决了?在下文你会看到GPU动画的一些问题。

GPU是如何合成图像的

GPU实际上可以看作一个独立的计算机,它有自己的处理器和存储器及数据处理模型。当浏览器向GPU发送消息的时候,就像向一个外部设备发送消息。

你可以把浏览器向GPU发送数据的过程,与使用ajax向服务器发送消息非常类似。想一下,你用ajax向服务器发送数据,服务器是不会直接接受浏览器的存储的信息的。你需要收集页面上的数据,把它们放进一个载体里面(例如JSON),然后发送数据到远程服务器。

同样的,浏览器向GPU发送数据也需要先创建一个载体;只不过GPU距离CPU很近,不会像远程服务器那样可能几千里那么远。但是对于远程服务器,2秒的延迟是可以接受的;但是对于GPU,几毫秒的延迟都会造成动画的卡顿。

浏览器向GPU发送的数据载体是什么样?这里给出一个简单的制作载体,并把它们发送到GPU的过程。

画每个复合层的图像

准备图层的数据

准备动画的着色器(如果需要)

向GPU发送数据

所以你可以看到,每次当你添加transform:translateZ(0)will-change:transform给一个元素,你都会做同样的工作。重绘是非常消耗性能的,在这里它尤其缓慢。在大多数情况,浏览器不能增量重绘。它不得不重绘先前被复合层覆盖的区域。

隐式合成

还记得刚才a元素和b元素动画的例子吗?现在我们将b元素做动画,a元素静止不动。

和刚才的例子不同,现在b元素将拥有一个独立复合层;然后它们将被GPU合成。但是因为a元素要在b元素的上面(因为a元素的z-index比b元素高),那么浏览器会做什么?浏览器会将a元素也多带带做一个复合层!

所以我们现在有三个复合层a元素所在的复合层、b元素所在的复合层、其他内容及背景层。

一个或多个没有自己复合层的元素要出现在有复合层元素的上方,它就会拥有自己的复合层;这种情况被称为隐式合成。

浏览器将a元素提升为一个复合层有很多种原因,下面列举了一些:

3d或透视变换css属性,例如translate3d,translateZ等等(js一般通过这种方式,使元素获得复合层)