evantianx / Bloooooooog

Place to record what I thought and learned
0 stars 0 forks source link

[译]GPU动画: #3

Open evantianx opened 7 years ago

evantianx commented 7 years ago

原文地址: GPU Animation: Doing It Right

大多数人已经知道现代浏览器会调用GPU来渲染部分网页,尤其是含有CSS动画的网页。举例来说,一个使用transform属性的CSS动画看起来要比使用lefttop属性的要顺滑流畅很多。那么你可能要问了,"我该如何利用GPU生成顺滑流畅的动画?",在大多数情况下,你可能会听到这样的建议,"用transform: translateZ(0)"或者wil-change: transform

这些属性在为GPU准备动画方面已经成为类似于我们在IE6中使用的zoom:1

但是很多时候单一Demo中非常漂亮顺滑的动画在实际网页中运行非常缓慢,同时会产生残影甚至造成崩溃。这是什么原因造成的?如何修复它? 看下文。

免责声明

在深入了解GPU硬件加速之前,我想首先声明的是:它只不过是一个hack。目前为止你不会在W3C文档中找到任何关于compositing如何工作,如何将某个元素显示地放置在合成层以及它本身的信息。它只是浏览器应用在当前执行任务的优化手段,而且各个浏览器厂商各自有各自的实现方式。

在这篇文章中你所学到的将不会是关于其工作机理的官方解释,而只是我个人的实验结果,附带有一些基本常识及浏览器底层工作原理。

硬件加速是如何工作的

在为GPU动画准备一个页面之前,我们必须理解在浏览器中各组件是如何协作的,而不是从杂七杂八的地方摘取某些建议。

假设我们的页面中有A和B两个元素,每个元素都有position: absolute和不同的z-index值。浏览器会调用CPU来渲染它,然后将结果图发送给GPU,而GPU最后将结果显示在屏幕上。

#a, #b {
  position: absolute
}

#a {
  left: 30px;
  top: 30px;
  z-index: 2;
}

#b {
  z-index: 1;
}

1

我们决定通过改变left属性以及CSS动画来改变A元素位置:

#a, #b {
  position: absolute
}

#a {
  left: 30px;
  top: 30px;
  z-index: 2;
  animation: move 1s linear;
}

#b {
  left: 50px;
  top: 50px;
  z-index: 1;
}

@keyframes move {
  from {left: 30px;}
  to {left: 100px;}
}

2

在这个案例中,对于动画的每一帧,浏览器都会重新计算元素的几何属性(也即reflow),重新渲染这个页面新状态的image(也即repaint,重绘),然后将其再次发送给GPU以展示在屏幕上。我们知道重绘是相当耗能的,好在每个现代浏览器会智能重绘页面中被修改的部分而不是整个页面。尽管浏览器在大多数情况下重绘时间非常短暂,我们的动画看起来依旧不是很流畅。

在每一步动画(甚至逐帧)回流(reflow)和重绘(repaint)整个页面听起来非常耗时,尤其是对于一个庞大而复杂的布局。若渲染两个单独的images会非常的省时省力——一个属于A元素,而另外一个属于除A元素之外的整个页面——然后简单地将这些images进行相对偏移即可。另一方面,用缓存元素来组成images会节省时间。这也正是GPU所表现出来的: 它可以用亚像素精度快速绘制images,这也使得动画变得顺滑无比。

为了优化compositing,浏览器必须保证要动画的CSS属性:

你可能会觉得topleft以及position: absolute,position: fixed并不依赖于元素的环境,其实情况并非如此。举例来说,left属性可能会接受一个百分比值,而这个百分比值依赖于.offsetParent的尺寸;除此之外,em,vh以及某些单位同样也依赖于它们所处环境。总之,transformopacity是唯二满足以上三个条件的CSS属性

让我们来用transform代替left产生动画:

#a, #b {
  position: absolute
}

#a {
  left: 30px;
  top: 30px;
  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);}
}

在这里,我们显示声明了我们的动画:它的起始位置,终止位置,持续时间等等。这提前通知了浏览器哪个CSS属性会被更新。由于浏览器会检测到(在动画属性中)没有属性会造成回流或者重绘,所以它可以优化compositing:绘制两个images作为合成层(compositing layers)并且将它们发送给GPU。

这样优化的优点有哪些?

这么看来一切都很清楚简单,对吗?我们难道会遇到什么问题吗?让我们来看看这个优化究竟是如何工作的。

你可能会感到惊讶,GPU实际上相当于一台独立的电脑。每一台现代设备的基本组成部分其实都可以看作拥有独立处理器,独立内存和数据处理模型的独立单位。而在浏览器方面,如同其他应用或者游戏,必须与GPU进行类似于它与外部设备一般的对话。

为了更好的理解上述工作原理,想想AJAX。假设你要用已经输入在网页表格中的数据来注册一个网页访问者。你不能仅仅跟远端服务器打个招呼,"嘿,把数据从表单控件和JS变量中取出然后存在数据库里面。" 因为远程服务器没有访问用户浏览器内存的权限。你应该这样做,将页面中的数据收集起来,按照某个易于解析数据格式(如JSON)作为有效数据发送给远程服务器。

在compositing中有着与如上类似的操作。GPU就像一个远程服务器,而浏览器首先创建一个有效数据然后将其发送给设备。当然,GPU并不是与CPU相隔千里之外;它就在附近。然而,鉴于一个远程服务器在很多情况下请求响应相隔2s是很正常的,而对于GPU数据传输,额外的3到5毫秒将会造成动画的跳动。

GPU有效数据长啥样?多数情况下,它包含了layer images,附带了额外信息如层尺寸,位移及动画变量等等。下面是关于如何创建playload及传输数据给GPU的大致描述:

正如你所看见的,每次你将transform: translateZ(0)或者will-change: transform属性添加给元素,实际上都相当于执行了上述操作。尽管重绘非常地耗能,同时很耗时。在大多数情况下,浏览器不能逐帧重绘。它只能绘制之前由新创建的合成层覆盖的区域。 3

隐式compositing

让我们回到我们的那个例子中(A,B元素)。之前我们使A产生了运动,而它是居于页面其余元素之上的。这样会生成两个合成层: 一个属于A元素,另外一个属于B元素及页面背景。

现在,让B元素动起来:

4

REFLOWS & REPAINTS: CSS PERFORMANCE MAKING YOUR JAVASCRIPT SLOW?