谈谈层合成(Composite)
- Web页面的大致渲染过程
- 什么是Composite?
- 从Node节点到RenderObject
- 从RenderObject到RenderLayer
- 从RenderLayer到GraphicsLayer
- Composite是怎么工作的?
- 隐式合成
- 内存消耗
- 利弊
- 优化技巧
- 避免隐式合成
- 仅用动画transform和opacity属性
- 尽可能使用CSS过渡和动画
Web页面的大致渲染过程
浏览器对于一个Web页面的展示大致可以认为经历了以下几个步骤。
- JavaScript:一般来说,我们会使用 JavaScript 来实现一些视觉变化的效果。比如做一个动画或者往页面里添加一些 DOM 元素等。
- Style:计算样式,这个过程是根据 CSS 选择器,对每个 DOM 元素匹配对应的 CSS 样式。这一步结束之后,就确定了每个 DOM 元素上该应用什么 CSS 样式规则。
- Layout:布局,上一步确定了每个 DOM 元素的样式规则,这一步就是具体计算每个 DOM 元素最终在屏幕上显示的大小和位置。web 页面中元素的布局是相对的,因此一个元素的布局发生变化,会联动地引发其他元素的布局发生变化。比如, 元素的宽度的变化会影响其子元素的宽度,其子元素宽度的变化也会继续对其孙子元素产生影响。因此对于浏览器来说,布局过程是经常发生的。
- Paint:绘制,本质上就是填充像素的过程。包括绘制文字、颜色、图像、边框和阴影等,也就是一个 DOM 元素所有的可视效果。一般来说,这个绘制过程是在多个层上完成的。
- Composite:渲染层合并,由上一步可知,对页面中 DOM 元素的绘制是在多个层上进行的。在每个层上完成绘制过程之后,浏览器会将所有层按照合理的顺序合并成一个图层,然后显示在屏幕上。对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。
当然今天主要谈的是层合成(Composite)。
什么是Composite?
我们先来看一个图。
从Node节点到RenderObject
DOM树中的每个Node节点都对应一个RenderObject,RenderObject 知道如何在屏幕上 paint Node 的内容。
从RenderObject到RenderLayer
一般来说,拥有相同的坐标空间的 RenderObject,属于同一个渲染层(RenderLayer)。RenderLayer 最初是用来实现 stacking contest(层叠上下文),以此来保证页面元素以正确的顺序合成(composite),这样才能正确的展示元素的重叠以及半透明元素等等。因此满足形成层叠上下文条件的 RenderObject 一定会为其创建新的渲染层,当然还有其他的一些特殊情况,为一些特殊的 RenderObject创建一个新的渲染层,比如
从RenderLayer到GraphicsLayer
某些特殊的渲染层会被认为是合成层(Compositing Layers),合成层拥有单独的 GraphicsLayer,而其他不是合成层的渲染层,则和其第一个拥有 GraphicsLayer 父层公用一个。
每个 GraphicsLayer 都有一个 GraphicsContext,GraphicsContext 会负责输出该层的位图,位图是存储在共享内存中,作为纹理上传到 GPU 中,最后由 GPU 将多个位图进行合成,然后 draw 到屏幕上,此时,我们的页面也就展现到了屏幕上。
GraphicsContext 绘图上下文的责任就是向屏幕进行像素绘制(这个过程是先把像素级的数据写入位图中,然后再显示到显示器),在chrome里,绘图上下文是包裹了的 Skia(chrome 自己的 2d 图形绘制库)
Composite是怎么工作的?
假设我们有一个包含A和B元素的页面,每个都有position: absolute和z-index应用于它的页面。浏览器将从CPU绘制图像,然后将生成的图像发送到GPU,GPU将其显示在屏幕上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <!DOCTYPE html> <html> <head> <title>GPU测试</title> <style type="text/css"> #A,#B{ width: 100px; height: 100px; position: absolute; } #A{ left: 30px; top: 30px; z-index: 2; background-color: red; } #B{ z-index: 1; background-color: blue; } </style> </head> <body> <div id="A">A</div> <div id="B">B</div> </body> </html> |
下面我们来进行动画处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <!DOCTYPE html> <html> <head> <title>GPU测试</title> <style type="text/css"> #A,#B{ width: 100px; height: 100px; position: absolute; } #A{ left: 30px; top: 30px; z-index: 2; animation: move 1s linear; background-color: red; } #B{ z-index: 1; background-color: blue; } @keyframes move { form {left: 30px;} to {left: 150px;} } </style> </head> <body> <div id="A">A</div> <div id="B">B</div> </body> </html> |
在这种情况下,对于每个动画帧,浏览器都必须重新计算元素的几何形状(即回流
在动画的每个步骤上回流和重绘整个页面,听起来确实很慢,尤其是对于大型而复杂的布局而言。如果仅绘制两个单独的图像(一个用于A元素,一个用于不包含该A元素的整个页面),然后简单地使这些图像相对于彼此偏移,将会更加有效。换句话说,绘制缓存元素的图像会更快。这正是GPU的亮点:它能够以亚像素精度非常快速地合成图像,从而为动画增添了性感的平滑度。
为了优化合成,浏览器必须确保动画CSS属性:
- 不会影响文档的流程。
- 不依赖于文档的流程。
- 不会导致重绘。
可能会认为top和left属性以及positions absolute和fixed并不依赖于元素的环境,但是事实并非如此。例如,一个left属性可能会收到一个百分比值,该百分比值取决于的大小offsetParent;同时,em,vh和其他单位取决于他们的环境。相反,transform和opacity是唯一满足上述条件的CSS属性。
1 2 3 4 | @keyframes move { form {transform: translateX(30px);} to {transform: translateX(150px);} } |
在这里,我们已经声明性地描述了动画:动画的开始位置,结束位置,持续时间等。这可以提前告知浏览器将更新哪些CSS属性。因为浏览器发现所有属性都不会导致回流或重绘,所以它可以应用合成优化:将两个图像绘制为合成层并将其发送到GPU。
这种优化的优点是什么?
- 我们获得了具有亚像素精度的柔滑流畅的动画,该动画在为图形任务特别优化的单元上运行。而且它运行非常快。
- 动画不再绑定到CPU。即使您运行非常繁琐的JavaScript任务,动画仍将快速运行。
一切看起来都非常简单明了,对吧?我们会遇到什么问题?让我们看看这种优化是如何工作的。
为了更好地了解其工作原理,请考虑使用AJAX。假设您要使用网站表单中输入的数据注册为网站用户。您不能仅仅告诉远程服务器,“嘿,只需从这些输入字段和JavaScript变量中获取数据并将其保存到数据库中即可。” 远程服务器无权访问用户浏览器中的内存。取而代之的是,您必须将页面中的数据收集到有效载荷中,该有效载荷应具有易于解析的简单数据格式(例如JSON),然后将其发送到远程服务器。
在合成过程中会发生非常相似的事情。由于GPU就像是一台远程服务器,因此浏览器必须先创建有效负载,然后再将其发送到设备。当然,GPU与CPU的距离不是数千公里。就在那儿。但是,尽管在许多情况下远程服务器请求和响应所需的2秒是可以接受的,但是GPU数据传输所花费的额外3到5毫秒将导致动画不稳定。
GPU有效负载是什么样的?在大多数情况下,它由图层图像以及其他指令(例如图层的大小,偏移量,动画参数等)组成。这大致上就是通过GPU进行有效负载和数据传输的过程:
- 将每个合成层绘制为单独的图像。
- 准备图层数据(大小,偏移,不透明度等)。
- 为动画准备着色器(如果适用)。
- 将数据发送到GPU。
隐式合成
让我们回到使用A和B元素的示例。之前,我们对A元素进行了动画处理,该元素位于页面上所有其他元素的顶部。这导致了两个合成层:一个包含A元素,一个包含B元素和页面背景。
现在,让我们为B元素设置动画:
我们遇到了一个逻辑问题。元素B应位于单独的合成层上,屏幕的最终页面图像应在GPU上合成。但是该A元素应该出现在元素B的顶部,并且我们还没有指定任何内容将A其提升到自己的层。
请记住一个大的免责声明:特殊的GPU合成模式不是CSS规范的一部分;这只是浏览器内部应用的一种优化。我们必须有A出现在B上面的顺序完全相同,所界定z-index。浏览器会做什么?
你猜对了!它将强制为元素A创建一个新的合成层A当然,还要添加另一个繁重的重绘:
这称为隐式合成:应按堆叠顺序将出现在合成元素上方的一个或多个非合成元素提升为合成层-即绘制成单独的图像,然后将其发送到GPU。
我们偶然发现隐式合成比您想像的要频繁得多。浏览器会出于多种原因将元素提升为合成层,其中只有几个原因:
- 3D转换:translate3d,translateZ依此类推。
, 和 元件。 - transform和opacity经由Element.animate()的动画。
- transform和opacity经由СSS过渡和动画的动画。
- position: fixed。
- will-change。
- filter。
内存消耗
再次提醒您,GPU是一台单独的计算机:不仅需要将渲染的图层图像发送到GPU,还需要存储它们以供以后在动画中重复使用。
单个复合层需要多少内存?让我们举一个简单的例子。尝试猜测要存储一个用纯色#FF0000填充的320×240像素矩形需要多少内存。
一个典型的Web开发人员会认为:“嗯,它是纯色图像。我将其另存为PNG并检查其大小。它应该小于1 KB。” 他们是完全正确的:作为PNG的图像大小为104字节。
问题在于,PNG与JPEG,GIF等一起用于存储和传输图像数据。为了将这种图像绘制到屏幕上,计算机必须将其从图像格式中解压缩,然后将其表示为像素阵列。因此,我们的示例图像将占用320 × 240 × 3 = 230,400 bytes计算机内存。也就是说,我们将图片的宽度乘以图片的高度即可得出图片中的像素数。然后,将其乘以3,因为每个像素都用三个字节(RGB)描述。如果图像包含透明区域,我们会乘以4,因为一个额外的字节需要描述透明度:(RGBA) 320 × 240 × 4 = 307,200 bytes。
浏览器始终将合成层绘制为RGBA图像。似乎没有有效的方法来确定元素是否包含透明区域。
让我们举一个可能的例子:一个带有10张照片的轮播,每张照片尺寸为800×600像素。我们决定在用户??交互(例如拖动)时在图像之间平滑过渡,因此我们will-change: transform为每个图像添加了图像。这将提前将图像提升为合成层,以便过渡在用户交互后立即开始。现在,计算多如何额外内存要求只是为了显示这样的圆盘传送带:800×600×4×10≈ 19 MB。
利弊
现在,我们已经学习了GPU动画的一些基础知识,下面我们来总结一下它的优缺点。
优点:
- 动画快速流畅,每秒60帧。
- 精心制作的动画可以在单独的线程中工作,并且不会被繁重的JavaScript计算所阻止。
- 3D转换“便宜”。
缺点:
- 需要额外的重绘以将元素提升为复合层。有时这很慢(例如,当我们进行全层重绘而不是增量重绘时)。
- 绘制的图层必须转移到GPU。根据这些层的数量和大小,传输也可能非常慢。这可能会导致低端和中端设备上的元素闪烁。
- 每个复合层都占用额外的内存。内存是移动设备上的宝贵资源。过多使用内存可能会导致浏览器崩溃。
- 如果您不考虑隐式合成,则重绘速度慢,额外的内存使用量和浏览器崩溃的机会非常高。
如您所见,尽管具有一些非常有用和独特的优势,但GPU动画仍然存在一些非常讨厌的问题。最重要的是重绘和过多的内存使用。因此,下面介绍的所有优化技术都将解决这些问题。
优化技巧
现在我们已经设置了环境,我们可以开始优化合成层。我们已经发现了两个主要的合成问题:额外的重绘(也会导致数据传输到GPU)和额外的内存消耗。因此,以下所有优化技巧都将针对这些问题。
避免隐式合成
这是最简单,最明显的技巧,但非常重要。让我提醒您,一个以上具有明确合成原因的所有非合成DOM元素(例如position: fixed,视频,CSS动画等)将被强制提升为它们自己的层,仅用于GPU上的最终图像合成。在移动设备上,这可能会导致动画开始非常缓慢。
仅用动画transform和opacity属性
transform和opacity的特性都不会影响或由正常流动或DOM环境(即,它们不会造成回流或重画,所以他们的动画可以被完全卸载到GPU)的影响。基本上,这意味着您只能有效地动画化移动,缩放,旋转,不透明度和仿射变换。有时,您可能希望使用这些属性来模拟其他动画类型。
尽可能使用CSS过渡和动画
我们已经知道的是动画transform,和opacity通过CSS过渡或动画将自动创建一个合成层和工作在GPU上。我们也可以通过JavaScript进行动画处理,但是必须先添加transform: translateZ(0)或添加动画,will-change: transform, opacity以确保元素具有自己的合成层。