这样使用 GPU 渲染 CSS 动画(转)

大多数人知道现代网络浏览器使用GPU来渲染部分网页,特别是具有动画的部分。 例如,使用transform属性的CSS动画看起来比使用lefttop属性的动画更平滑。 但是如果你问,“我如何从GPU获得平滑的动画?”在大多数情况下,你会听到像“使用transform:translateZ(0)will-change:transform这样的建议。

这些属性已经成为像我们如何在Internet Explorer 6下使用zoom:1(如果你明白我的意思的话)在准备GPU的动画或说合成加速,浏览器厂商喜欢这样叫它。

但有时,在简单demo中运行的又好又平滑的动画,放在一个真实的网站上运行的时候却很慢,会造成视觉假象,甚至导致浏览器崩溃。 为什么会发生这种情况?** 我们如何解决它?** 让我们试着去了解。

一个免责声明

在我们深入GPU加速之前,我想告诉你最重要的事:这是一个 giant hack。 你不会在(至少现在)W3C的规范中找到任何关于合成加速是如何运作,关于如何在合成层上显式地放置一个元素,甚至是关于合成加速本身。 它只是浏览器应用在执行某些任务时的优化,而且各个浏览器厂商都通过自己的方式去实现这种优化。

在本文中你将学到的一切并不是对合成加速是如何运作的官方解释,而是我用自己的一些常识和不同浏览器系统工作原理的知识去实验的结果。可能会有一些小错误,有些过段时间可能会改变 —— 我已经提醒过你了哦!

合成加速是如何运作

要准备一个GPU动画的页面,我们必须了解其在浏览器中如何工作,而不只是随便的去遵循从网上或从这篇文章中得到的建议。

假设我们有一个包含AB元素的页面,每个元素都有position:absolute和一个不同的z-index。 浏览器将从CPU绘制它,然后将生成的图像发送到GPU,最后将显示在屏幕上。



A
B

这样使用 GPU 渲染 CSS 动画(转)_第1张图片

我们已经决定通过A元素的left属性和CSS动画来使其运动起来:



A
B

这样使用 GPU 渲染 CSS 动画(转)_第2张图片

在这种情况下,对于每个动画帧,浏览器必须重新计算元素的几何形状(即重排),和渲染页面新状态下的图像(即重绘),然后再次发送到GPU将其显示在屏幕上.我们都知道重新绘制是非常耗性能的,但每个现代浏览器都非常聪明地只重绘页面中改变的区域,而不是整个页面。 虽然浏览器在大多数情况下可以非常快速地重绘,但我们的动画仍然不够平滑。

在动画的每个步骤(甚至递增)重排和重绘整个页面听起来真的很慢,特别是对于一个大且复杂的布局。比较有效的方法是绘制两个单独的图像 —— 一个用于A元素,一个用于没有A元素的整个页面 —— 然后简单地让这些图片相对于彼此偏移,换句话说,合成缓存元素的图像将会加速。 这正是GPU的亮点所在:它能够以亚像素精度快速构图,为动画增添了平滑感。

要优化合成,浏览器必须确保动画的CSS属性:

  • 不影响文档流,
  • 不依赖于文档流,
  • 不会造成重绘。

大家可能会认为absolutefixedtop和 left属性不依赖于元素环境,但事实并非如此。例如,left属性可以接收取决于定位父级大小的百分比值; 同样的,emvh和其他单位取决于他们的环境。 相反,transformopacity是唯一满足上述条件的CSS属性。

让我们通过transform来替换left实现动画效果:



A
B

在这里,我们以声明的方式描述了动画:它的开始位置,结束位置,持续时间等。这会告诉浏览器提前更新CSS属性。 因为浏览器没有看到任何会导致重排或重绘的属性,它可以通过合成优化:将两个图像绘制为合成图层并将其发送到GPU。

这种优化的优点是什么?

  • 我们可以通过亚像素精度得到一个运行在特殊优化过的单位图形任务上的平滑动画,并且运行非常快。
  • 动画不再绑定到CPU。 即使你运行一个非常复杂的JavaScript任务,动画仍然会很快运行。

一切似乎都很清楚和容易,对吧? 但我们可能遇到什么问题? 让我们看看这个优化是如何工作的.

GPU是一个单独的计算机,这可能会让你感到惊讶。但这是正确的:每个现代设备的一个重要部分实际上是一个独立的单元,有自己的处理器和自己的内存和数据处理模型。 和任何其他应用程序或游戏一样,浏览器需要与GPU交谈。

为了更好地了解这是如何工作的,想想AJAX。 假设你想通过他们在网络表单中输入的数据去计算网站访问者数量。 你不能只告诉远程服务器,“嘿,从这些输入字段和JavaScript变量中获取数据并将其保存到数据库。”远程服务器不能访问用户浏览器中的内存。 相反,您必须将页面中的数据收集后转化为可轻松解析的简单数据格式(如JSON),并将其发送到远程服务器。

在合成过程中也会发生类似的情况。 因为GPU就像一个远程服务器,浏览器必须首先创建一个有效负载,然后将其发送到设备。 当然,GPU不是距离CPU几千公里远; 它就在那里。 但是,尽管远程服务器请求和响应所需的2s在多数情况下是可接受的,但是一个GPU数据传输额外耗费的35毫秒将导致"janky"动画。

什么是GPU有效负载? 在大多数情况下,它包括层图像,以及它附加的数据,如图层的大小,偏移量,动画参数等。这里的GPU有效负载和传输数据大致像是:

  • 将每个合成图层绘制为单独的图像
  • 准备图层数据(大小,偏移,不透明度等)
  • 准备动画的着色器(如果适用)
  • 将数据发送到GPU

正如你可以看到的,每次你给元素添加transform:translateZ(0)will-change:transform属性,你都启动了相同进程。 重绘成本是非常高昂的,运行甚至更慢。 在大多数情况下,浏览器无法增量重绘。 它必须用新建的复合层去绘制之前被覆盖的区域:

这样使用 GPU 渲染 CSS 动画(转)_第3张图片

隐式合成

让我们回到我们的AB元素的例子。 之前,我们让A元素在页面上其他所有元素之上动起来了。 这导致有两个合成层:一个是A元素所在的层和一个B元素所在的页面背景层。 现在,让我们来让B元素动起来:

这样使用 GPU 渲染 CSS 动画(转)_第4张图片

我们遇到了一个逻辑问题。 元素B应该在单独的合成层上,并且屏幕的最终页面图像应该在GPU上组成。 但是A元素应该出现在元素B的顶部,而且我们没有指定任何关于提升A元素自身层级的东西。

请记住这个提醒:特殊的GPU合成模式不是CSS规范的一部分; 它只是一个浏览器在内部应用的优化。 我们通过定义z-indexA必须按照顺序出现在B的顶部。 那么浏览器会做什么?

你猜到了! 它将强制为元素A创建一个新的合成图层 — 并添加另一个重绘图,当然:

这样使用 GPU 渲染 CSS 动画(转)_第5张图片

这被称为隐式合成:一个或多个非合成元素应该出现在层叠顺序中被提升的复合层之上 —— 即绘制为分离的图像,然后发送到GPU。

我们偶然发现隐式合成比你想象的更频繁。 浏览器会将元素提升为合成层的原因有很多,其中包括:

  • 3D transforms: translate3dtranslateZ等等;
  • , 和