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

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

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

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

一个免责声明

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

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

合成加速是如何运做

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

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

<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>

咱们已经决定经过A元素的left属性和CSS动画来使其运动起来:浏览器

<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属性:

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

你们可能会认为absolutefixedtop和 left属性不依赖于元素环境,但事实并不是如此。例如,left属性能够接收取决于定位父级大小的百分比值; 一样的,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是一个单独的计算机,这可能会让你感到惊讶。但这是正确的:每一个现代设备的一个重要部分其实是一个独立的单元,有本身的处理器和本身的内存和数据处理模型。 和任何其余应用程序或游戏同样,浏览器须要与GPU交谈。

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

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

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

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

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

隐式合成

让咱们回到咱们的AB元素的例子。 以前,咱们让A元素在页面上其余全部元素之上动起来了。 这致使有两个合成层:一个是A元素所在的层和一个B元素所在的页面背景层。 如今,让咱们来让B元素动起来:

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

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

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

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

咱们偶然发现隐式合成比你想象的更频繁。 浏览器会将元素提高为合成层的缘由有不少,其中包括:

  • 3D transforms: translate3dtranslateZ等等;
  • <video>,<canvas> 和 <iframe> 元素;
  • 经过Element.animate()而有transform动画和opacity属性的元素;
  • 经过СSS transitions 和 animations而有transform动画和opacity属性的元素;
  • position: fixed;
  • will-change;
  • filter;

更多缘由描述在Chromium项目的“CompositingReasons.h”文件中。

看起来GPU动画的主要问题好像是意想不到的重绘。但其实否则,更大的问题实际上是...

内存消耗

另外一个温柔的提醒,GPU是一个单独的计算机:它不只须要发送渲染层图像到GPU,并且还需存储它们,以便稍后在动画中重用。

单个复合层须要多少内存? 让咱们举个简单的例子。 尝试猜想须要多少内存来存储一个用纯#FF0000颜色填充的320 × 240px矩形。

一个典型的Web开发人员会认为,“嗯,这是一个纯色的图像。 我将它保存为PNG并检查其大小。 它应该小于1KB。“他们是绝对正确的:这个图像做为PNG的大小是104字节。

问题是PNG,以及JPEG,GIF等,用于存储和传输图像数据。 为了将这样的图像绘制到屏幕上,计算机必须解压图像格式,而后将其表示为像素阵列。 所以,咱们的样本图像将须要320×240×3 = 230,400字节的计算机内存。 也就是说,咱们将图像的宽度乘以其高度以得到图像中的像素数。 而后再乘以3,由于每一个像素由三个字节(RGB)描述。 若是图像包含透明区域,咱们将其乘以4,由于须要额外的字节来描述透明度:(RGBa):320×240×4 = 307,200字节

浏览器老是将合成图层绘制为RGBa图像。 彷佛没有有效的方法来肯定一个元素是否包含透明区域。

让咱们举一个可能的例子:一个轮播有10张照片,每张是800 × 600px。 咱们来实现一个图片间平滑过渡的交互,好比拖拽,因而咱们为每一个图像添加了will-change:transform。 这将提早将图像提高为复合层,以便在用户交互时当即开始过渡。 如今来计算下须要多少额外的内存来显示这样的轮播:800×600×4×10≈19MB

须要19 MB的额外内存来渲染单个控件! 若是你是一个在制做一个单页面应用程序网站的现代Web开发人员的话,须要不少动画控制,视差效果,高分辨率图像和其余视觉加强功能,那么每页额外100到200 MB 只是开始,还须要添加隐式合成去混合,你最终会以用尽设备上的可用内存结束。

此外,在许多状况下,这个内存将被浪费,并显示很是相同的结果:

对桌面客户端用户还好,但对于使用移动设备的用户来讲是很坑的。 首先,大多数现代设备具备高密度屏幕:将复合层图像的权重乘以4到9。其次,移动设备没有台式机那么多的内存。 例如,一个不是很旧的iPhone 6附带1 GB的共享内存(即用于RAM和VRAM的内存)。 考虑到这个内存的至少三分之一被操做系统和后台进程使用,另外三分之一被浏览器和当前页面使用(高度优化的页面没有大量框架的最佳状况),咱们最多还剩下大约200到300 MB内存给GPU效果。 iPhone 6是一个至关昂贵的高端设备;比它便宜的手机内存更少。

你可能会问,“有可能在GPU中存储PNG图像以减小内存占用吗?”从技术上来讲,是的,这是可能的。 惟一的问题是GPU逐像素地绘制屏幕,这意味着它必须一次又一次地为每一个像素解码整个PNG图像。 我怀疑在这种状况下的动画会比每秒1帧更快。

值得一提的是,GPU特定的图像压缩格式确实存在,可是它们在压缩比方面甚至还比不上PNG或JPEG,而且它们的使用受硬件支持的限制。

优势和缺点

如今咱们已经学习了一些GPU动画的基础知识,让咱们总结它的优势和缺点。

优势

  • 动画快速,流畅,每秒60帧。
  • 一个正确制做的动画在单独的线程中运做,而且不会被大量JavaScript计算阻止。
  • 3D变换是“便宜的”。

缺点

  • 添加剧绘是须要提高元素层级到复合层。 有时这是很是慢的(即咱们获得一个全层重绘,而不是一个增量)。
  • 绘图层必须传输到GPU。 根据这些层的数量和尺寸,转移也可能很是慢。 这可能致使元素在低端和中端市场设备上闪烁。
  • 每一个复合层都消耗额外的内存。 内存是移动设备上的宝贵资源。 过多的内存使用可能会致使浏览器崩溃。
  • 若是你不考虑隐式合成,而使用慢速重绘,除了额外的内存使用,浏览器崩溃的概率也很是高。
  • 咱们会有视觉假象,例如在Safari中的文本渲染,在某些状况下页面内容将消失或变形。

正如你能够看到,GPU动画不只有一些很是有用和独特的优点,也有一些很是讨厌的问题。最主要是重画和过分的内存占用; 所以,下面涵盖的全部优化技术都将解决这些严重的问题。

浏览器设置

在咱们开始优化,咱们须要了解将帮助咱们检查页面上的复合层,和提供关于优化效率反馈的工具。

SAFARI

Safari的Web Inspector有一个很棒的“Layers”边栏,用来显示全部复合层及其内存消耗,以及合成的缘由。 查看此侧边栏:

  • 在Safari中,使用⌘+⌥+ I打开Web Inspector。若是不起做用,请打开“Preferences”→“Advanced”,打开“Show Develop Menu in menu bar”选项,而后重试。
  • 当Web Inspector打开时,选择“Elements”选项,而后在右侧边栏中选择“Layers”。
  • 如今,当你单击一个在主“Elements”窗中的DOM节点时,您将看到所选元素(若是使用合成)和全部后代复合图层的图层信息。
  • 单击一个后代图层以查看其合成的缘由。 浏览器会告诉你为何将这个元素移动到本身的合成图层上。

CHROME

Chrome的开发者工具备一个相似的面板,但你必须先启用标志:

  • 在Chrome中,前往chrome:// flags /#enable-devtools-experiments,并启用"Developer Tools experiments"标记。
  • 使用⌘+⌥+ I(在Mac上)或Ctrl + Shift + I(在PC上)打开开发者工具,而后单击右上角的图标并选择“Settings”菜单项。
  • 转到“Experiments”窗格,而后启用“Layers”面板。
  • 从新打开开发者工具。 你如今应该看到“Layers”面板。

此面板将当前页面的全部活动合成图层显示为树。 选择图层时,您将看到相应的信息,例如其大小,内存消耗,重绘数量和合成缘由。

优化技巧

如今咱们已经设置了咱们的环境,咱们能够开始优化合成层。 咱们已经肯定了合成的两个主要问题:额外的重绘,这也会使数据传输到GPU,以及额外的内存消耗。 所以,下面全部的优化技巧都主要针对这个问题。

避免隐式合成

这是最简单和最显而易见的技巧,也是很是重要的技巧。 让我提醒你,全部非合成的DOM元素具备显式合成缘由(例如, position: fixedvideo,CSS动画等))将被强制提高到本身的图层,只是为了在GPU上合成最后的图像。 在移动设备上,这可能会致使动画开始很是缓慢。

让咱们举个简单的例子:

A元素会在用户交互时动起来。 若是你在“Layers”面板中查看此页面,你看不到额外的图层。 可是在点击“播放”按钮后,你会看到更多的图层,这些图层将在动画完成后当即删除。 若是你在“Timeline”面板中查看此过程,你会看到动画的开始和结束进行了大面积的重绘:

浏览器作了如下几步:

  • 在页面加载后,浏览器找不到任何合成理由,所以它选择最佳策略:在单个背景图层上绘制页面的整个内容。
  • 经过点击“播放”按钮,咱们明确地添加了合成给元素A —— 一个具备transform属性的过渡动画。 可是浏览器肯定元素A在层叠顺序中低于元素B,因此它也将B提高到本身的合成层(隐式合成)。
  • 提高到合成层老是会致使重绘:浏览器必须为元素建立新的纹理,并将其从上一层中删除。
  • 新图层必须传输到GPU,以便用户在屏幕上看到的最终图像合成。 根据层数,纹理的大小和内容的复杂性,从新绘制和数据传输可能须要大量的时间来执行。 这就是为何咱们有时会看到一个元素在动画开始或结束的时候闪烁。
  • 在动画完成后,咱们从A元素中删除合成的理由。浏览器看到它不须要浪费资源去合成,因此它回到最佳策略:保持页面的整个内容在一个单一的层,这意味着它必须在背景上绘制AB层(另外一个重绘),并将更新的纹理发送到GPU。 如上面的步骤,这可能会致使闪烁。

为了摆脱隐式合成问题和减小视觉假象,我建议以下:

  • 尽量在z-index中保持动画对象。 理想状况下,这些元素应该是body元素的直接子元素。 固然,当动画元素嵌套在DOM树内部而且依赖于正常流时,这在标记中是不必定的。 在这种状况下,您能够克隆元素并将其放在body中仅用于动画。
  • 你能够给浏览器一个提示,你将要去合成使用与具备will-changeCSS属性的元素。 经过在元素上设置此属性,浏览器将(但不老是)提早将其提高到合成层,以便动画能够平滑地开始和中止。 可是不要滥用这个属性,不然你的内存消耗会大大增长!

只有动画TRANSFORM 和 OPACITY属性

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×100px100×100×4 = 40,000字节),而#b只有10×10px10×10×4 = 400字节), 使用transform:scale(10)缩放到100×100px。 由于#b是一个复合层,因为will-change属性,`transform'会在最终图像绘制期间彻底出如今GPU上。

这个诀窍很简单:使用widthheight属性减小复合层的物理大小,而后使用transform:scale(...)'扩展它的纹理。固然,这个技巧只简单粗暴地减小了实色层的内存消耗。若是你想让一张大照片动起来,你能够把它缩小5%10%`,而后把它缩放一级; 用户可能看不到任何差别,你还将节省几兆字节的宝贵内存。

尽量使用 CSS TRANSITIONS和动画

咱们已经知道动画的transformopacity是经过CSS transitions 或animations 自动建立一个合成层,并在GPU上工做。 咱们也能够经过JavaScript添加动画,可是咱们必须首先添加transform:translateZ(0)will-change:transform,`opacity',以确保元素得到本身的合成层。

JavaScript animation happens when each step is manually calculated in a requestAnimationFrame callback. Animation via Element.animate() is a variation of declarative CSS animation.

JavaScript动画发生在requestAnimationFrame的每一次回调手动计算时。 经过“Element.animate()”实现的动画是变量声明的CSS动画变体。

一方面,经过CSS transition 或animation 建立一个简单且可重用的动画是很容易的; 另外一方面,在建立复杂的动画时,使用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将是一个元素与射线图像的容器。 每一个射线将以特定角度旋转。

html, body {
  overflow: hidden;
  background: #a02615;
  padding: 0;
  margin: 0;
}

.sun {
  position: absolute;
  top: -75px;
  left: -75px;
  width: 500px;
  height: 500px;
  animation: sun-spin 10s linear infinite;
}

.sun-ray {
  width: 250px;
  height: 40px;
  background: url(//sergeche.github.io/gpu-article-assets/images/ray.png) no-repeat;

  /* align rays with sun center */
  position: absolute;
  left: 50%;
  top: 50%;
  margin-top: -20px;
  transform-origin: 0 50%;
}

$rays: 12;
$step: 360 / $rays;

@for $i from 1 through $rays {
  .sun-ray:nth-of-type(#{$i}) { transform: rotate(#{($i - 1) * $step}deg); }
}

@keyframes sun-spin {
  from { transform: rotate(0); }
  to   { transform: rotate(360deg); }
}

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

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

在“图层”面板中再次查看网页的标记。 咱们能够更容易旋转整个太阳容器。 所以,这个容器被提高为一个合成层,并被绘制成一个单一的大纹理图像,而后发送到GPU。 可是因为咱们的简化,纹理如今包含无用的数据,即光线之间的间隙。

此外,无用的数据在大小上比有用的数据大得多! 但这不是咱们合理利用内存资源的最好方式。

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

  • 整个太阳容器:500×500×4≈977 KB
  • 仅十二个光线:250×40×4×12≈469 KB

内存消耗将减小两倍。 要作到这一点,咱们必须将每一个射线的动画分开,而不是动画的容器。 所以,只有光线的图像将被发送到GPU; 它们之间的差距不会占用任何资源。

咱们必须使咱们的标记复杂化,以便独立地对光线进行动画处理,此时CSS将成为障碍。 咱们已经对光线的初始旋转使用了transform,并且咱们必须从彻底相同的角度开始动画,并进行360deg转动。 基本上,咱们必须为每一个射线建立一个单独的@keyframes部分,这是不少网络传输的代码。

编写一个简短的JavaScript来处理光线的初始放置,而且容许咱们对动画,光线数量等进行微调.

const container = document.querySelector('.sun');
const raysAmount = 12;
const angularVelocity = 0.5;
const rays = createRays(container, raysAmount);

animate();

function animate() {
    rays.forEach(ray => {
        ray.angle += angularVelocity;
        ray.elem.style.transform = `rotate(${ray.angle % 360}deg)`;
    });
    requestAnimationFrame(animate);
}

function createRays(container, amount) {
    const rays = [];
    const rotationStep = 360 / amount;
    while (amount--) {
        const angle = rotationStep * amount;
        const elem = document.createElement('div');
        elem.className = 'sun-ray';
        container.appendChild(elem);
        rays.push({elem, angle});
    }
    return rays;
}

新动画看起来与前一个相同,可是实际上比上一个少了2倍的内存消耗。

不只仅是这样, 在布局组成方面,这个动画太阳不是主要元素,而是一个背景元素。 光线没有任何清晰的对比元素。 这意味着咱们能够向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)

const container = document.querySelector('.sun');
const raysAmount = 12;
const angularVelocity = 0.5;
const downscale = 0.1;
const rays = createRays(container, raysAmount, downscale);

animate();

function animate() {
    rays.forEach(ray => {
        ray.angle += angularVelocity;
        ray.elem.style.transform = `rotate(${ray.angle % 360}deg) scale(${ray.scale})`;
    });
    requestAnimationFrame(animate);
}

function createRays(container, amount, downscale) {
    const rays = [];
    const rotationStep = 360 / amount;
    while (amount--) {
        const angle = rotationStep * amount;
        const elem = document.createElement('div');
        elem.className = 'sun-ray';
        container.appendChild(elem);

        let scale = 1;
        if (downscale) {
            const origWidth = elem.offsetWidth, origHeight = elem.offsetHeight;
            const width = origWidth * (1 - downscale);
            const height = origHeight * (1 - downscale);
            elem.style.width = width + 'px';
            elem.style.height = height + 'px';
            scale = origWidth / width;
        }

        rays.push({elem, angle, scale});
    }
    return rays;
}

注意,咱们只改变了元素的大小; PNG图像的大小保持不变。 由DOM元素建立的矩形将呈现为GPU的纹理,而不是PNG图像。

太阳射线在GPU上的新组成大小如今是225×36×4×12≈380 KB(它是469 KB)。 咱们将内存消耗下降了19%,而且获得了很是灵活的代码,咱们能够经过缩减来得到最佳的质量 - 内存比。 所以,经过增长动画的复杂性,看起来这么简单,咱们已经减小了内存消耗977 ÷ 380≈2.5倍!

我想你已经注意到这个解决方案有一个重大的缺陷:动画如今在CPU上工做,并且会被大量JavaScript计算阻止。 若是你想更熟悉如何优化GPU动画,我留个小做业。在这个demo中 Codepen of the sun rays,让太阳射线动画彻底在GPU上工做,还要保证像原来的例子中的内存效率和弹性。 在评论中发布你的示例以获取反馈。

课程学习

  • 优化 Chaos Fighters页面的研究使我彻底从新思考现代网页的开发过程。 如下是个人主要原则:
  • 始终与客户和设计师讨论网站上的全部动画和效果。 它会很大程度上影响页面的标记,以便于更好的合成。
  • 从一开始就注意复合层的数量和大小 —— 特别是经过隐式合成建立的层。 浏览器开发工具中的“Layers”面板是你最好的朋友。
  • 现代浏览器不只将合成大量地用于动画,并且还用于优化绘制页面元素。 例如,position:fixediframevideo元素使用合成。
  • 合成层的尺寸可能比层的数量更重要。 在某些状况下,浏览器会尝试减小复合层的数量(请参阅“GPU加速复合在Chrome中的”图层压缩“部分); 这防止了所谓的“层爆炸”而且减小了存储器消耗,特别是当层具备大的交叉点时。 可是有时,这种优化具备负面影响,例如当很是大的纹理比几个小的层消耗更多的存储器时。 为了绕过这个优化,我向每一个元素添加一个小的,惟一的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上建立它的图像。

相关文章
相关标签/搜索