关于层合成(composite)

596 阅读10分钟

浏览器渲染流程

首页,我们需要了解一下浏览器的页面展示过程:除去网络资源获取的步骤,我们理解的 Web 页面的展示,一般可以分为构建 DOM 树、构建渲染树、布局、绘制、渲染层合成几个步骤。

image.png

  • 构建 DOM 树:浏览器将 HTML 解析成树形结构的 DOM 树,一般来说,这个过程发生在页面初次加载,或页面 JavaScript 修改了节点结构的时候。
  • 构建渲染树:浏览器将 CSS 解析成树形结构的 CSSOM 树,再和 DOM 树合并成渲染树。
  • 布局(Layout):浏览器根据渲染树所体现的节点、各个节点的CSS定义以及它们的从属关系,计算出每个节点在屏幕中的位置。Web 页面中元素的布局是相对的,在页面元素位置、大小发生变化,往往会导致其他节点联动,需要重新计算布局,这时候的布局过程一般被称为回流(Reflow)。
  • 绘制(Paint):遍历渲染树,调用渲染器的 paint() 方法在屏幕上绘制出节点内容,本质上是一个像素填充的过程。这个过程也出现于回流或一些不影响布局的 CSS 修改引起的屏幕局部重画,这时候它被称为重绘(Repaint)。实际上,绘制过程是在多个层上完成的,这些层我们称为渲染层(RenderLayer)。
  • 渲染层合成(Composite):多个绘制后的渲染层按照恰当的重叠顺序进行合并,而后生成位图,最终通过显卡展示到屏幕上。

什么是渲染层合成

在 DOM 树中每个节点都会对应一个渲染对象(RenderObject),当它们的渲染对象处于相同的坐标空间(z 轴空间)时,就会形成一个 RenderLayers,也就是渲染层。渲染层将保证页面元素以正确的顺序堆叠,这时候就会出现层合成(composite),从而正确处理透明元素和重叠元素的显示。

这个模型类似于 Photoshop 的图层模型,在 Photoshop 中,每个设计元素都是一个独立的图层,多个图层以恰当的顺序在 z 轴空间上叠加,最终构成一个完整的设计图。

浏览器渲染原理

image.png

1、渲染对象(RenderObject)

一个 DOM 节点对应了一个渲染对象,渲染对象依然维持着 DOM 树的树形结构,一个渲染对象知道如何绘制一个 DOM 节点的内容。

2、渲染层(RenderLayer)

这是浏览器渲染期间构建的第一个层模型,处于相同坐标空间(z轴空间)的渲染对象,都将归并到同一个渲染层中,因此根据层叠上下文,不同坐标空间的的渲染对象将形成多个渲染层,以体现它们的层叠关系。所以,对于满足形成层叠上下文条件的渲染对象,浏览器会自动为其创建新的渲染层。能够导致浏览器为其创建新的渲染层的,包括几类常见的情况:根元素 document,有明确的定位属性(relative、fixed、sticky、absolute),opacity < 1等等。

DOM 节点和渲染对象是一一对应的,满足以上条件的渲染对象就能拥有独立的渲染层。当然这里的独立是不完全准确的,并不代表着它们完全独享了渲染层,由于不满足上述条件的渲染对象将会与其第一个拥有渲染层的父元素共用同一个渲染层,因此实际上,这些渲染对象会与它的部分子元素共用这个渲染层。

3、图形层(GraphicsLayer)

我们日常生活中所看到屏幕可视效果可以理解为:由多个位图通过 GPU 合成渲染到屏幕上,而位图的最小单位是像素。如下图:

image.png

那么位图是怎么获得的呢,Graphics Layer 便起到了关键作用,每个 Graphics Layer 都有一个 Graphics Context, 位图是存储在共享内存中,Graphics Context 会负责将位图作为纹理上传到GPU中,再由GPU进行合成渲染,此时,我们的页面也就展现到了屏幕上,如下图:

image.png

所以 GraphicsLayer 是一个重要的渲染载体和工具,但它并不直接处理渲染层,而是处理合成层。

4、合成层(CompositingLayer)

满足某些特殊条件的渲染层,会被浏览器自动提升为合成层。合成层拥有单独的 GraphicsLayer,而其他不是合成层的渲染层,则和其第一个拥有 GraphicsLayer 的父层共用一个。

那么一个渲染层满足哪些特殊条件时,才能被提升为合成层呢?这里列举了一些常见的情况:

  • 设置 transform: translateZ(0),注意它必须是 translateZ。如果使用 translateX 或 translateY,元素将会被绘制在普通文档流中 demo。
  • position:fixed。
  • will-change 该属性告诉浏览器该元素会有哪些变化,这样浏览器可以提前做好对应的优化准备工作。当该属性的值为 opacity、transform、top、left、bottom、right 时。
  • video、canvas、iframe 等元素。
  • 等等

兼容性

提升合成层的最好方式是使用 CSS 的 will-change 属性。而 will-change 设置为 opacity、transform、top、left、bottom、right 可以将元素提升为合成层。

先来看看 will-change 的浏览器支持情况,点击查看

image.png

当然,对于个别不支持的浏览器,我们使用 translateZ(0) 来解决,点击查看

image.png

如何去观察页面的图层结构呢,您需要在 Chrome 开发工具中打开自定义菜单,然后在 More tools 中选择 Layers 选项。

image.png

可以先看一个demo

image.png

关于隐式合成

隐式合成就是特定场景下,存在会被默认提升为合成层的情况。具体我们可以再看一下之前图层结构的demo,只要我们把 B 和 C 的 z-index 交换一下你就会发现 B 被隐式的提升为合成层了。

image.png

只是z-index导致的么如果我们再调整一些 B 的位置,保证 B 和 C、D 没有交集,那么你会发现这次 B 并没有被隐式提升为合成层了。

image.png

结论:一个或多个非合成元素应出现在堆叠顺序上的合成元素之上,会被提升为合成层。

层压缩与层爆炸

按我们刚刚说的例子,如果在堆叠顺序底部有一个合成元素,那是不是会导致大量堆叠顺序上的元素被提升为合成层?其实大部分情况下,我们在开发过程中并不会去关注层合成的问题,那么我们刚刚说的情况就会有发生的可能性。当这些不符合预期的合成层达到一定量级时,就会发生层爆炸,这会导致你的页面占用大量的内存资源,带来一些无法预期的情况。

这是我们不想看到的情况,面对这个问题浏览器也有相对应的一些解决方案,如果多个渲染层同一个合成层重叠时,这些渲染层会被压缩到一个图层中,以防止由于重叠原因导致出现的层爆炸。

B、C、D 本来都应该被提升为合成层,但是由于发生层压缩,它们会渲染在一个图层里面。

image.png

一些建议

使用 transform 和 opacity 来实现动画

在我们日常开发中经常会实现一些动画,有时候我们可能会选择改变 top/left 去实现,那么这个节点的渲染会发生在普通文档流中。而使用 transform 和 opacity 实现动画能够让节点被放置到一个独立合成层中进行渲染绘制,动画不会影响其他图层,并且 GPU 渲染相比 CPU 能够更快,这会让你的动画变的更加流畅,我们来看看他们的区别。

通过 left 来实现动画:

image.png

通过 transform 来实现动画:

image.png

可以看到通过 transform 来实现动画,页面的 fps 能够稳定在 60 左右,而通过 left 来实现存在波动,fps 会变低,且一直波动,这会影响用户体验指标。

直接看demo

注:查看帧率的界面唤醒方法

image.png

谨慎使用 will-change

我认为除非你的元素的真的存在某个属性马上会发生变化,例如 transform,你可以使用 will-change: transform 告知浏览器,根据您打算更改的元素,浏览器可能可以预先安排,元素的改变和渲染速度都会变得更快。可是这些属性可能会给你带来一些副作用,我们来看一个demo

任何带有 position: fixed 或者 position: absolute 的子元素将会相对于设置了 will-change: transform 的元素进行相对定位。所以在你使用的时候需要确保这种意料之外的情况不会造成影响。

除此之外浏览器用来为 will-change 属性做的更进一步的优化常常会耗费更多的资源,如果你将它施加在过多属性上显然是一个浪费,更甚者非常过度的使用可能会造成页面相应速度的变慢或者直接崩溃。

image.png

减少隐式合成

虽然隐式合成从根本上来说是为了保证正确的图层重叠顺序,但具体到实际开发中,隐式合成很容易就导致一些无意义的合成层生成,归根结底其实就要求我们在开发时约束自己的布局习惯,避免踩坑。

比如我们可以把动画节点的 z-index 属性值设置得大一些,让层叠顺序高过于页面其他无关节点就行。当然并不是盲目地设置 z-index 就能避免,有时候 z-index 也还是会导致隐式合成,这个时候可以试着调整一下文档中节点的先后顺序直接让后边的节点来覆盖前边的节点,而不用 z-index 来调整重叠关系。方法不是唯一的,具体方式还是得根据不同的页面具体分析。

减小合成层绘制区域

合成层的绘制区域大小,很大程度上影响了它的内存占用,举个简单的例子,分别画两个尺寸一样的 div,但实现方式有点差别:一个直接设置尺寸 100x100,另一个设置尺寸 10x10,然后通过 scale 放大 10 倍,并且我们让这两个 div 都提升为合成层:

demo

image.png

image.png

利用 Chrome Devtools 查看这两个合成层的内存占用后发现,.size100 内存占用是 160 kb,而 .size10 是 1.6kb,差距十分明显。这是因为 .size10 是合成层,现在完全在 GPU 上执行。在用户看不到任何区别的前提下,能够节省大量的内存。当然这个例子只适用于这种纯色的场景,我们主要是看下绘制区域对于内存占有的影响。

合成层的利弊

好处:

  • 开启硬件加速,合成层的位图会交由 GPU 合成,相比 CPU 处理要快。
  • 合成层发生 repaint 的时候,不会影响其他图层。
  • 对于 transform 和 opacity 效果,不会触发 layout 和 paint。

一些问题:

  • 如果我们把所有渲染工作都交给 GPU,在现有的优化下,它会导致渲染内存占用比大幅度提升,反而出现负面的效果。
  • 另外隐式合成容易产生大量我们意料之外的合成层,过大的内存占用,会让页面变的卡顿,性能优化适得其反。

大多数人都很喜欢使用3D属性 translateZ(0) 来进行所谓的硬件加速,以提升性能。但我们还需要切实的去分析页面的实际性能表现,不断的改进测试,这样才是正确的性能优化途径。