谈谈层合成(Composite)

谈谈层合成(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创建一个新的渲染层,比如 overflow!= visible 的元素。根据创建 RenderLayer 的原因不同,可以将其分为常见的 3 类(NormalRenderLayer,OverflowClipRenderLayer,NoRenderLayer)。

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

在这里插入图片描述
在这种情况下,对于每个动画帧,浏览器都必须重新计算元素的几何形状(即回流 reflow),渲染页面新状态的图像(即重绘 repaint),然后将其再次发送到GPU以在屏幕上显示。我们知道,重新绘制的性能非常昂贵,但是每个现代的浏览器都足够聪明,只需要重新绘制页面的更改区域,而不是整个页面。尽管浏览器在大多数情况下可以非常快速地重绘,但我们的动画仍然不流畅。

在动画的每个步骤上回流和重绘整个页面,听起来确实很慢,尤其是对于大型而复杂的布局而言。如果仅绘制两个单独的图像(一个用于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依此类推。