Olmsted

正确地使用 GPU 动画 – Smashing Magazine

Olmsted · 2017-01-26翻译 · 353阅读 原文链接

很多人知道现代网页浏览器会使用GPU渲染部分网页,尤其是包含动画的部分。例如,一个使用transformCSS属性的动画,看起来就比使用left或者top属性的动画更加流畅。但是如果你想问,“怎样才能通过GPU获得更流畅的动画呢?” 大多数情况你得到的答案会像“使用transform:translate(0)或者will-change:transform。”

我们曾经在IE6使用的zoom:1去将动画准备好给GPU——或者称为图像合成(compositing),因为浏览器厂商会这么称呼,如今这些属性已经成为与其类似的存在(但愿你能理解 = =)

然而有的时候动画效果在简单的demo里面运行得很好很平滑,但是到了实际网页中表现缺很慢,出现一些老的视觉效果甚至使浏览器崩溃。为什么会这样呢?如何修复?让我们来一探究竟。

###巨不负责的申明

在我们深入了解GPU图像合成之前,有一件很重要的事情需要交待:这是一个巨大的hack行为。你在 W3C的规范协议上将不会找到任何有关影像合成如何工作的东西,也不会明确告诉你如何将一个元素放到一个图像合成的层面或者自身进行图像合成(译注:就是使用GPU呗)。这只是一个优化,浏览器应用于执行某些特定任务,每个浏览器厂商都以自己各自的方式实现。

图像合成的工作原理

要准备一张给GPU的动画页面,我们要了解如何在浏览器中工作,不只是遵循一些网上或者这篇文章里随意的建议。

比方说我们有一个页面,AB元素,每个元素都应用了position: absolute和不同的z-index。浏览器会将它在CPU中画出来,然后将得到的图像传给GPU,GPU会将图像显示在屏幕上。

<style>
#a, #b {
 position: absolute;
}

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

#b {
 z-index: 1;
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>

我们决定通过CSS animation属性和变动left的值,使A元素实现动画。

<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 { left: 30px; }
 to { left: 100px; }
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>

在这种情况下,对于每个动画帧,浏览器必须重新计算单元的几何结构(即回流),渲染页面的新状态的图像(即重绘)然后重新发送给GPU显示在屏幕上。我们知道,重绘是很浪费性能的,不过所有现代浏览器是足够聪明,它们只将有改变的区域重绘,而不是重绘整个页面。尽管浏览器可以在大多数情况下很快地重绘,然而我们的动画还不是很流畅。

在动画(即使是递增的)的每一步回流和重绘整个页面看起来真的很慢,尤其是一个庞大而复杂的布局。如果只画出两个单独的图像将会大大提高效率——个有A元素的和另一个没有A元素的整张页面。换句话说,对缓存了的元素进行图像合成会使速度变快。这正是GPU的光芒:它能很快使用的亚像素精度*构成图像,为动画增添了性感的流畅度。

为了使图像合成最优化,浏览器需要确保CSS动画属性——

  • 不得影响文档流

  • 不得依赖文档流

  • 不得触发重绘

或许有人认为,position被设置为absolutefixedtopleft属性,是不依赖这个元素所在的环境的,然而事实并非如此。比如,一个left属性可能会取一个百分比的值,这个百分比基于.offsetParent的大小;除此之外,emvh等其它单位也依赖于它们所处的环境。所以,只有transformopacity在CSS属性中是能够满足上述条件的。

我们来用transform替代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属性会被更新。因为浏览器领会到这些属性都不该回流或者重绘,所以浏览器应用了一个图像合成的优化:绘制两个图像合成层并将它们传给GPU。

这种优化的优点是什么?

  • 我们得到了一个有着亚像素级精度的非常流畅的动画,这个动画运行在一个为图像任务进行特殊优化的单元上。而且它运行得很快。

  • 动画不再绑定到CPU上。即使你运行一个非常密集的JavaScript任务,动画仍然运行得很快。

一切都看似美好而简单,不是么?那么我们会面临什么问题呢?让我们来见识一下这个优化是如何实现的。

也许你不会相信,但是GPU就是一个独立计算机。没错,现代设备一个基本的部分其实是一个独立的单元,这个单元有着自己的处理器和存储器以及数据处理模块。而浏览器,跟任何其他的app或者游戏一样,需要和GPU对话以实现使用外部的设备。

为了更好理解这是如何运行的,可以联想一下AJAX。假如你想使用用户输入在网页表单例的数据去注册一个网站访问账户,你可以直接高速远程服务器:“喂,就把那些input输入框里面和JavaScript变量的数据拿出来,然后保存到数据库中吧。”远程服务器没有进入用户浏览器空间的权限。所以取而代之的是,你必须从页面中搜集这些数据,将其以可以被轻松解析的简单数据形式(例如JSON),传入有效载荷(payload),然后发送到远程服务器。

类似的事情也发生在图像合成的过程中。因为GPU就像一个远程的服务器,而浏览器先建了一个有效载荷(payload),然后将它发送到了设备上。当然,GPU并非跟CPU相隔千山万水,相反就在CPU的身边。不过,当对远程服务器发起的请求和响应需要2秒钟都在大部分情况下被接受的时候,GPU数据传输需要的额外的2-5毫秒对动画的影响就很微不足道了。

那GPU的有效载荷(payload)像什么呢?多数情况下,它是由层状图像(layer images)组成的,与之一起的还有其他类似图层的大小,偏移度,动画参数等。以下大致就是是有效载荷的如何被建立以及GPU之间的数据传输过程。

  • 每个合成的图像层都绘制一个单独的图像。

  • 准备图层数据(大小,偏移度,透明度等)。

  • 为动画准备着色器(如果可以应用)。

  • 将数据传给GPU。

正如你所见到的那样,每次你将神奇的transform: translateZ(0)will-change: transform 属性添加到元素上的时候,你启动了相同的进程。由于重绘是非常消耗性能的,这会导致速度更加慢。多数情况下,浏览器是不能做增量的重绘的。它必须在这个在先前已经被覆盖的区域,用一个新的合成图像去覆盖:

###隐式合成

让我们回到先前AB元素的那个例子中。之前我们对在页面内其他所有元素之上的A元素设置了动画。使用两个图像合成层的表现为:一个层是A元素的,一个层是B元素的,还有一个页面背景的层。

现在,我们转而设置B的动画:

现在我们面临一个逻辑问题。B元素应当是在一个单独的图像合成层上,而最终屏幕上的页面图像应当在GPU上被合成。但是A元素应当是在B元素的上层的,可我们没有进行任何声明让A提升到自己所在的图像层。

在前面免责声明中讲过:特殊GPU合成模式并非CSS标准的一部分,只不过是浏览器应用内部的优化。既然我们必须让A正确地出现在B的上方,那就通过定义z-index实现。这样浏览器将会这么做呢?

没错!浏览器会为A元素强行建立一个新的合成图像层——然后再一次艰难地重绘,当然:

这叫做隐式合成(implicit composing):一个或者多个按照堆叠顺序应当出现在合成元素上方的非合成元素——即绘制一个单独的图像发送给GPU。

我们在隐式合成面前蹒跚不前的次数远超你想象。浏览器将一个元素提升到影像合成层的原因有很多,一下只是其中一部分:

  • 3D 转换: translate3d, translateZ 等;

  • video, canvasiframe 元素;

  • 通过 Element.animate() 操作 transformopacity的动画;

  • 通过СSS transitionanimation操作 transformopacity的动画;

  • position: fixed;

  • will-change;

  • filter;

更多原因请见Chromium项目中“CompositingReasons.h”文件的描述。

似乎GPU动画的主要问题是不可预期的高消耗重绘。然而并非如此。更大的问题是——

内存消耗

再次温馨提示,GPU是独立计算的:不仅需要将渲染层的图像发送给GPU,而且还需要存储它们以供后续在动画中再次使用。

那么一个单独的合成图层需要消耗多少内存呢?我们来看个简单的实例。想象一下一个320*240像素、填充色为#FF0000的矩形需要消耗多少内存吧。

一个典型的web开发人员认为:“嗯,这是一个单色图像。我会保存为PNG格式并且检查它的大小。它应该不到1KB。”完全正确:作为一个PNG的这张图片的大小就是104个字节。

问题是,png和jpeg、gif等,是用于存储和传输图像数据。绘制这样一个图像在屏幕上,电脑必须将它从图像格式解析,然后以像素的数组形式展现。所以我们举例的图片将消耗320 × 240 × 3 = 230,400 字节的计算机内存。我们把图像的宽度乘以它的高度得到图像中像素的数量。然后,我们把它乘以3,因为每一个像素都是被三个字节(RGB)。如果图像包含透明区域,我们把它乘以4,因为需要一个额外的字节来描述透明度:(RGBa):320×240×4 = 240字节

浏览器总是以RGBa图像的形式绘制合成图层。似乎没有有效的方法来判断一个元素是否包含透明区域。

让我们更有可能出现的例子:一组旋转木马效果的10张图片,每个大小800×600像素。我们决定图像在用户交互之间的平稳过渡、比如拖拽,所以我们为每个图像添加will-change:transform。这会提前将图像提升到合成图层上,这样随着用户的操作就会立刻还是过度动画。现在,计算需要多少额外的内存来显示这样一个旋转木马效果吧:800×600×4×10≈19MB

需要19 MB的额外的内存来渲染一个单一的控制!如果你是一个现代web开发人员,在创建一个单页面应用程序的网站中,有很多动画控制,视差效果,高分辨率图像和其他视觉增强效果,这样每个页面额外的100到200MB 仅仅是个开始。加上隐式合成(承认吧-你以前就没有考虑过),最终你的设备所有的可用内存都将被这个页面占据。

不仅如此,在很多情况下,这些内存将被浪费,表现出非常相似的结果:

这对于桌面端用户或许不是什么大不了的,但是对许移动端用户就太伤了。首先,大多数当代设备都有高密度的屏幕:合成层图像的负担要乘以4到9倍。第二,移动设备没有桌面设备有那么多的内存。比如一个还不算过时的 iPhone6 搭载的是1GB的共享内存(即内存是RAM和VRAM共享的),考虑到至少三分之一的内存要被操作系统和后台进程占用,另外三分之一又被浏览器和当前的页面(一个高度优化的最好状态的页面,而且没有成吨的框架)占用,我们最多仅剩200-300MB给GPU效果了。何况iPhone 6是一个挺贵的高端设备,便宜的手机内存更小。

或许你会问,“是否可以将PNG图像存储在GPU中,用于减少内存占用?” 严格意义上来说,这是可行的。然而唯一的问题就是GPU 在屏幕上一个像素一个像素地绘制。这意味着它需要一次又一次地为整个PNG图像解码。我怀疑这样让动画效果的提升能否达到每秒一帧。

GPU规范的 图像压缩格式确实存在,然而这毫无意义,在压缩比例方面还远不及PNG或者JPEG,并且使用效果还受制于硬件支持。

优势与不足

既然我们已经了解了一些GPU动画的基础,让我们来总结一下有何优劣。

优势

  • 动画更加流畅,达到60帧每秒

  • 一个精心制作的有效动画是在一个单独的线程中运行,不会受到高消耗的JavaScript运算的影响。

  • 3D转换代价小

不足

  • 额外的重绘需要将元素提升到合成图层。有时候这会很缓慢(也就是,当我们重新绘制整个图层,而不是绘制一个增长的部分)。

  • 绘制好的图层需要转换到GPU中。受这些图层的总数量和大小的决定,转换过程也可能会很慢。因此可能导致中低端设备上元素出现闪烁。

  • 每个合成图层会消耗额外内存。内存在移动设备上是很宝贵的资源。极度的内存使用会导致浏览器崩溃

  • 如果你不考虑隐式合成,那么重回速度的变慢、额外内存的使用和浏览器崩溃的可能性都会变得很高

  • 我们会得到异常的图像,例如Safari中的文字渲染和页面内容会消失或者扭曲。

正如你所见到的,尽管有这些很有用的很独特的优势,GPU动画还是有一些非常讨厌的问题。最主要的是重绘和过度内存的使用;因此,以下优化方案将着眼解决这些问题。

浏览器设置

开始优化之前,需要了解我们即将工具,它将帮助我们检查页面上的合成图层,提供优化效果的准确反馈。

Safari

Safari的检查器有非常好的"layer"栏,它显示所有合成图层和对应的内存消耗,并且还有产生图像合成的原因。设置如下:

1.在Safari中, ⌘ + ⌥ + I大开检查面板。如果这样不行,打开 “Preferences” → “Advanced,”开启“Show Develop Menu in menu bar”选项,再进行尝试。(译注:没有mac,不晓得这些选项对应的是什么中文)

2.检查器打开后,选择“Element”面板,然后在右侧栏选择“Layers”

3.现在如果在主“Element”面板点击一个DOM节点,可以看到被选择了的元素的图层信息(如果使用了图像合成的话)以及后代合成的图层。

4.点击自图层可以看到它进行图像合成的原因。你会知道为什么浏览器决定将这个元素一刀它自己的图像合成层。

Safari with Web Inspector

(大图戳此处)

Chrome

Chrome 的开发工具有类似的面板,但是需要开启先启用。

  1. 在Chrome中,前往 chrome://flags/#enable-devtools-experiments,开启“开发者工具实验性功能 ”。

  2. 使用⌘ + ⌥ + I (mac) or Ctrl + Shift + I (PC) 打开开发工具,然后点击右上方如下的图标打开'Setting'菜单项。

DevTools settings icon

  1. 前往"Experiment"面板, 启用"Layer"面板

  2. 再次打开开发工具。你就能看到“Layer”面板

Chrome with DevTools

(View large version)

这个面板以树的形式,展示了当前页面所有被激活的合成图像的图层。当你选择一个图层,你可以看到类似大小,内存消耗的信息,还有重绘次数和进行图像合成的原因。

优化技巧

现在我们配置好了环境,可以开始合成层的优化了。我们已经确定了在图像合成方面两个主要的问题:导致数据会传递给GPU的额外的重绘,还有额外的内存消耗。所以,下面的优化技巧将会着眼于这两个问题上。

避免隐式合成

这是最简单和最明显的技巧,但很重要。在此提醒,所有在显式合成的DOM元素(例如,positon:fixed,video,CSS animation 等)上的,不进行图像合成的DOM元素,将会被强制提升到自身所在的图层的位置,只是为了在GPU上的最终图像合成。。在移动设备上,这可能会导致动画开始非常缓慢。

让我们举个简单的例子:

A元素应该在用户交互时呈现动画。如果你在“layer panel”面板中查看此页面,你会看到没有额外的图层。但是在点击“play”按钮后,你会看到更多的图层,这些图层将在动画完成后立即删除。如果在“Timeline”面板中查看此过程,可以看到动画的开始和结束伴随着大面积的重绘:

Chrome timeline

(View large version)

以下为浏览器一步接一步的操作:

1.在页面加载后,浏览器没有找到任何合成的原因,所以它选择最佳策略:在单个背景图层上绘制页面的整个内容。

2.通过单击“播放”按钮,我们明确地添加了合成到元素A - 一个转换过程(transition)与transform属性。但是浏览器确定元素A在层叠顺序中低于元素“B”,所以它也将B转换为其自己的合成层(隐式合成)。

3.提升到合成层一定导致重绘:浏览器必须为元素创建新的纹理,并从之前的层中删除它。

4.新图层图像必须传输到GPU,以便用户在屏幕上看到最终的图像合成。根据层数,纹理的大小和内容的复杂程度,重新绘制和数据传输可能需要大量的时间来执行。这就是为什么我们有时看到一个元素会在开始或者完成的时候有闪烁的动画。

5.在动画完成后,我们从A元素中删除了合成的原因。浏览器再一次看到它不需要浪费资源在合成上,所以它回到最佳策略:保持页面的整个内容在一个单一的层,这意味着它必须绘制'A'和'B'回到背景层(另一次重绘),并将更新的纹理发送到GPU。如上面的步骤,这可能会导致闪烁。

为了摆脱隐式合成问题和减少视觉效果瑕疵,我建议如下:

  • 尝试让动画对象在z-index的层级尽量高。理想情况下,这些元素应该是body元素的直接子元素。当然,当动画元素嵌套在DOM树内部并且依赖于正常流时,这在标记中并不总是可以实现的。在这种情况下,可以克隆元素并将其放在`body'中,仅用于动画使用。

  • 可以给浏览器一个提示,使用will-change CSS属性高速浏览器你将进行图像合成。通过在元素上设置此属性,浏览器将(并不总是)提前将其提升到合成图层,以便动画可以平滑地开始和停止。但是不要滥用这个属性,否则你的内存消耗会大大增加!

只使用transformopacity进行动画

transformopacity属性能保证既不影响也不受正常流或DOM环境的影响(即它们不会导致回流或重绘,因此其动画可以完全卸放到GPU上)。基本上,这意味着你可以有效地运动,缩放,旋转,不透明度和仿射变换。有时你可能想要用这些属性模拟其他动画类型。

以一个很常见的例子:一个背景颜色的转换。基本方法是添加一个transition属性:

<div id="bg-change"></div>
<style>
#bg-change {
 width: 100px;
 height: 100px;
 background: red;
 transition: background 0.4s;
}

#bg-change:hover {
 background: blue;
}
</style>

在这种情况下,动画将完全在CPU上工作,并在动画的每个步骤中引起重绘。但是我们可以使这样的动画在GPU上工作:不是让background-color属性产生动画,而是在顶部添加一个图层然后对其不透明度进行动画:

<div id="bg-change"></div>
<style>
#bg-change {
 width: 100px;
 height: 100px;
 background: red;
}

#bg-change::before {
 background: blue;
 opacity: 0;
 transition: opacity 0.4s;
}

#bg-change:hover::before {
 opacity: 1;
}
</style>

这个动画会更快更流畅,但请记住,它可能导致隐式合成,并需要额外的内存。不过在这种情况下,可以大大减少内存的消耗。

减小复合层的尺寸

看看下面的图片。注意什么差异没有?

这两个合成图层是视觉上相同的,但第一个大小40,000字节(39 KB),第二个只有400字节 - 小100倍。为什么?看看代码:

<div id="a"></div>
<div id="b"></div>

<style>
#a, #b {
 will-change: transform;
}

#a {
 width: 100px;
 height: 100px;
}

#b {
 width: 10px;
 height: 10px;
 transform: scale(10);
}
</style>

区别在于#a的物理大小是100×100像素(100×100×4 = 40,000字节),而#b只有10×10像素(10×10×4 = 400字节),但是使用transform:scale(10)缩放到100×100像素。因为#b是一个复合层,由于will-change属性,现在`transform'过程在最终图像绘制期间完全在GPU上发生。

窍门很简单:使用widthheight属性减少复合层的物理大小,然后使用transform:scale(...)扩展它的大小。当然,这个技巧仅仅为简单的纯色层显著减少了内存消耗。但是,如果你想动画一张大照片,你可以缩小它5到10%,然后把它放大;用户可能看不到任何差异,但你将节省几兆字节的宝贵内存。

只要可能,就使用CSS Transitions和Animation

我们已经知道,通过CSS transition或animation实现的'transform'和'opacity'的动画会自动创建一个合成层,并在GPU上工作。我们也可以通过JavaScript动画,但我们必须首先添加transform:translateZ(0)will-change:transform,opacity',以确保元素获得自己的合成层。

当每个动画动作在requestAnimationFrame中手动计算的时候, JavaScript 动画被触发。通过 Element.animate() 触发的动画是一个是声明性CSS动画的变体。

一方面,通过CSS转换或动画创建一个简单和可重用的动画是很容易的;另一方面,创建一个轨迹花式的复杂动画的时候,使用JavaScript比使用CSS容易很多。此外,JavaScript也是与用户交互的唯一方式。

哪一个更好?我们可以只使用通用JavaScript库来实现一切动画吗?

基于CSS的动画有一个非常重要的特性:它完全在GPU上工作。 因为声明了动画应该如何开始和结束,浏览器可以在动画开始之前准备好所有需要的指令,并将它们发送到GPU。 在命令式JavaScript的情况下,浏览器知道的所有当前帧的状态。 为了平滑的动画,我们必须在主浏览器线程中计算新帧,并且每秒将其发送到GPU至少60次。 除了这些计算和发送数据比CSS动画慢得多的因素外,它们还取决于于主线程的工作负载:

在上面的图中,你可以看到当主线程被密集的JavaScript计算阻塞时会发生什么。 CSS动画不受影响,因为新帧是在单独的线程中计算的,而JavaScript动画必须等待大量计算完成,然后计算新的帧。

因此,尽量使用基于CSS的动画,特别是对于加载和进度指示器。 不仅是它更快,但它不会被大量的JavaScript计算阻止。

优化实例:

这篇文章是我为Chaos Fighters研究和开发网页的结果。 这是一个响应式的促销页面的手机游戏,有着很多动画。 当我开始开发的时候,我唯一知道的是如何实现基于GPU的动画,但我不知道它的工作原理。 因此,第一个里程碑式意义的页面就导致iPhone 5 ——当时最新的苹果手机——在页面加载后几秒钟内崩溃。 现在,即使在性能更差的设备上,这个页面工作也是正常的。

让我们来探索一下我认为的这个网站最有趣的优化。

在页面的最顶部是游戏的描述,伴随类似红色太阳光线的东西在背景中旋转。 这是一个无限循环,无交互的旋转器——用简单的CSS动画实现,是一个不错的选择。 第一个(有误导性质)的尝试是保存太阳光线的图像,将其作为img元素放在页面上,并使用无限的CSS动画旋转:

似乎一切正常工作。但是太阳图像太大了。移动用户不会很高兴。

仔细看看图像。 基本上,它只是来自图像中心的几条光线。 光线是相同的,所以我们可以保存单个光线的图像,并重用它来创建最终的图像。 我们将最终得到单射线图像,其比初始图像小一个数量级。

对于这种优化,我们必须使标记语言复杂一点:.sun将是一个容器的元素与射线图像。 每个射线将以特定角度旋转。

视觉结果将是相同的,但网络传输的数据的量将更低。然而,复合层的尺寸保持相同:500×500×4≈977KB。

为了简单,我们的例子中的太阳光线的大小是相当小,只有500×500像素。 在真实的网站上,应用到的设备有不同尺寸(移动,平板电脑和台式机)和像素密度,最终的图片大约是3000×3000×4 = 36 MB! 而这只是页面上的一个动画元素。

在"layer"面板中再次查看网页的标注。 我们让自己更容易旋转整个太阳容器。 导致了这个容器被提升为一个合成层,并被绘制成一个单一的大纹理图像,然后传给GPU。 但是由于我们的简化,纹理现在包含无用的数据:光线之间的间隙。

不仅如此,无用的数据在大小上比有用的数据大得多! 这不是最好的方式来消耗我们非常有限的内存资源。

这个问题的解决方案与我们网络传输的优化相同:仅将有用数据(即光线)发送到GPU。 我们可以计算出我们要保存的内存量:

  • 整个太阳容器:500 × 500 × 4 ≈ 977 KB

  • 仅12条辐射线:250 × 40 × 4 × 12 ≈ 469 KB

    内存消耗将减少两倍。要做到这一点,我们必须使每个单独射线设置动画,而不是设置动画的容器。因此,只有光线的图像将被发送到GPU;它们之间的间隙不会占用任何资源。

我们必须使我们的标记复杂化,以便独立地对光线进行动画处理,在CSS将更多地成为阻碍。 我们已经对光线的初始旋转使用了transform属性,我们必须从完全相同的角度开始动画,并进行360度旋转。 基本上,我们必须为每个光线创建一个单独的@keyframes代码部分,这部分会有很多的网络传输量。

相比之下,编写一个简短的JavaScript来处理光线的初始放置,并且允许我们对动画,光线数量等进行微调,这将更容易。

新动画看起来与前一个相同,但是内存消耗的两倍。

还没完。 在布局组成方面,这个动画太阳不是主要元素,而是一个背景元素。 光线没有任何清晰的对比元素。 这意味着我们可以向GPU发送较低分辨率的光线纹理并随后将其放大,这使得我们又减少一点内存消耗。

让我们尝试将纹理的大小减少10%。 光线的物理尺寸将为250×0.9×40×0.9 = 225×36像素。 为了使光线看起来像250×20,我们必须将其放大250÷225≈1.111倍。

我们将为.sun-ray添加一行代码——background-size:cover,以便使背景图片自动调整为元素的大小,我们将为动画光线添加transform:scale(1.111)的属性。

注意,我们只改变了元素的大小; PNG图像的大小是不变的。由DOM元素创建的矩形将以GPU的纹理渲染,而不是以PNG图像渲染。

太阳射线在GPU上的新组成大小现在是225×36×4×12≈380 KB(原先是469 KB)。 我们将内存消耗降低了19%,并且得到了非常灵活的代码,我们可以通过缩减来获得最佳的“质量-内存”比。 因此,开始增加一些简单的复杂程度,我们将内存消耗减少了977÷380≈2.5倍!

我想你已经注意到这个解决方案有一个重大的缺陷:动画现在在CPU上进行计算工作,可能被大量JavaScript计算阻塞。 如果你想进一步了解GPU动画优化,我推荐一些额外的研究作业。 Fork Codepen of the sun rays的代码,并使太阳射线动画完全在GPU上工作,但是要求像原来例子一样的内存使用率和灵活程度。 在评论中提交您的示例以获取反馈。

课后总结

对于 Chaos Fighter 页面的优化研究使得我彻底重新对开发现代网页的过程进行了思考。以下是我总结的主要原则:

  • 永远要和客户或者设计师对网页的动画和效果进行协商。这会显著影响到页面使用的语句,并且促使更好的图像合成。

  • 从一开始就注意复合层的数量和大小——特别是被隐式合成创建的层。浏览器开发工具中的“Layers”面板是您最好的朋友。

  • 现代浏览器大量使用图像合成不仅仅是为了动画,而是优化页面元素的绘制。 例如,position:fixediframevideo元素就是使用合成的。

  • 合成层的尺寸可能比层的数量更重要。在某些情况下,浏览器会尝试减少复合层的数量。 (可参考 “GPU Accelerated Compositing in Chrome“中的“Layer Squashing”部分); 这防止了所谓的“层数爆炸”并且减少了内存消耗,特别是当图层之间具有大的交叉点时。 但是有时,这种优化也具有负面影响,例如非常大纹理的图层比几个小的层消耗更多的内存。为了绕过这个优化部分,我向每个元素添加一个小的,单独唯一的translateZ()值,例如translateZ(0.0001px)translateZ(0.0002px)等。浏览器将确定元素位于3D空间中的不同平面,并因此跳过优化。

  • 你不能只是添加transform:translateZ(0)will-change:transform到随意任何元素,以提高动画性能或摆脱视觉瑕疵。 GPU合成有许多缺点和利弊要被权衡考虑。 当不使用得太奔放的时候,图像合成合会降低整体性能,最坏的情况下会导致浏览器崩溃。

请允许我再次向您提示我们的免责声明:并没有GPU图像合成的官方规范,每个浏览器解决相同的问题的方式也是不一样的。 本文的某些部分可能在几个月后就过时了。 例如,Google Chrome开发人员正在探索如何减少CPU到GPU数据传输的开销,其中就有特殊共享内存,它的复制开销为零。 并且Safari已经能够将简单元素(例如具有background-color的空DOM元素)的绘图委托给GPU,而不是在CPU上创建它的图像

不管怎样,我希望这篇文章帮助你更好地了解浏览器如何使用GPU来渲染的,这样就可以创建优秀的网站,并且能在所有设备上快速运行。

(rb, vf, il, yk, al)

译者Olmsted尚未开通打赏功能

相关文章