-
Notifications
You must be signed in to change notification settings - Fork 0
探究"层"的使用
作者:黄春华
前端每天都在和渲染打交道,比如改变大小,改变颜色,或者插入一个新节点,都会促使屏幕上的显示内容发生变化,而这些变化都会体现在层上。优化层的使用作为是主要的性能优化点之一,主要工作就是通过了解层的产生与合并的原因进行合理优化,所以了解层的运作机制就好比读懂武器说明一样重要。
在了解什么是层之前,我们先来了解一下,从Html Parse
->GraphicsLayer Tree
在这些操作过程中到底都发生了一些什么?
这是一个复杂的过程:下图简单的讲述了这个过程。
从上文看见整个形成过程中,只有两种层,一种是RenderLayer
(负责DOM子树),一种是GraphicsLayer
(负责RenderLayer子树),对两者形成的条件进行比较
RenderLayer | GraphicsLayer |
---|---|
页面元素的根目录 | #document |
RenderObject 具有position 样式属性的。 |
RenderLayer 覆盖在一个同级GraphicsLayer 之上,RenderLayer 的z-index 大于GraphicsLayer 。 |
RenderObject 有透明效果 |
RenderLayer 使用CSS动画、opacity < 1 |
RenderObject 具有overflow,alpha或者反射效果的节点 |
RenderLayer 具有Reflection 样式属性。 |
RenderObject 使用Canvas2D和3D(WebGL)技术的RenderObject 节点 |
RenderLayer 为canvas ,并满足三个条件 |
RenderObject 是video 节点 |
RenderLayer 是video并有一个有效源 |
RenderObject 具有css filter 样式属性 |
RenderLayer 使用了硬件加速CSS Filters技术 |
RenderObject 具有transform 样式属性 |
RenderLayer 具有CSS 3D属性 |
opacity:1
是不能提升成为GraphicsLayer
的
fixed元素本身并不会产生单独的
GraphicsLayer
,当body
的内容产生溢出可以滚动的时,或者它覆盖在一个GraphcisLayer
之上时,才会成为GraphicsLayer
更加详细形成
GraphicsLayer
的原因请参考source codeCompositingReasons.cpp
可以看的出,GraphicsLayer
比RenderLayer
定义的更加严谨,在满足一定条件的情况下RenderLayer
可以转换成GraphicsLayer
,为什么要有RenderLayer
和GraphicsLayer
,本身RenderLayer
就可以承载渲染所需要的渲染条件了,但是GraphicsLayer
存在是为更加高效的进行渲染。
- 数量上
GraphicsLayer
的数量比RenderLayer
数量更少,在进行一些页面元素的复杂操作时,需要尽可能少的触发Paint
但又不能在#document
上触发一个Paint
。所以对RenderLayer
在进行合理分组得到的GraphicsLayer
显然更符合需求。 - 图层操的高效,无论是
GraphicsLayer
还是RenderLayer
都会经历一次Paint
的过程(GraphicsLayer
本身来自于RenderLayer),观察Performance
就可以观察到有几个GraphicsLayer
就有几次Paint
,但是一旦GraphicsLayer
形成,只要层内容本身不变,对单个图层进行位置(top,right,bottom,left)变换、透明度或者是3D transform的此类操作的性能就体现出来
GraphicsLayer(top/right/bottom/left) | RenderLayer(top/right/bottom/left) | 优势 |
---|---|---|
![]() |
![]() |
没有Paint |
GraphicsLayer(opacity) | RenderLayer(opacity) | 优势 |
![]() |
![]() |
没有paint |
GraphicsLayer(background/transform2d) | RenderLayer(background/transform2d) | 优势 |
![]() |
![]() |
N/A |
GraphicsLayer(width) | RenderLayer(width) | 优势 |
![]() |
![]() |
N/A |
GraphicsLayer(transform3d) | RenderLayer(transform3d) | 优势 |
![]() |
N/A | 具有`transform3d`属性的`RenderLayer`必定是`GraphicsLayer`,所以没有比较对象,但是可以看出,`transform3d`没有任何`Layout`和`Paint`,这一点上和`GraphisLayer`的`opacity`表现一致,都是最节省资源的方式 |
从上述场景进行对比,可以看到,GraphicsLayer来进行transform和opacity非常节省资源,主要的差别在于Layout与Paint
GPU是专门处理图像的,对于GPU来说合并图像比重绘图像要高效的多
- Recalculate Style: 此阶段用以与CSSDOM结合计算所有可见节点的样式信息。
事件与 DOM 解析不同,该时间线不显示单独的
Parse CSS
条目,而是在这一个事件下一同捕获解析和CSSOM
树构建,以及计算的样式的递归计算。参考:Constructing the Object Model
-
Layout:计算可见节点在设备
viewport
的确切位置和大小,这个就是layout的任务参考:Render-Tree Construction, Layout, and Paint。 -
Update Layer Tree:检查以及更新
GrapicLayerTree
的结构的,每一次用户操作,如:滚动、动画、改变长宽、显示隐藏节点都会触发Update Layer Tree
。 -
Paint:需要计算每一个
GraphicsLayer
中的每一个像素的颜色,并把它打印在一个SKPicture上(就是一张图)。 -
Composite Layers:将所有的
GraphicsLayer
进行组合,把它们最后Draw
在一张图像上。最后光栅化到屏幕上。与Update Layer Tree
一样,每次有操作都会触发Composite Layers
这里需要注意的是,Composite Layers的过程远远比我们这里说的要复杂,并且涉及到许多GPU操作,这里我们不做过多的深入探讨。
Draw
和Paint
。这两字很容易混淆,首先字面理解,Paint
对应的彩色的绘画,如油彩画,而draw对应的是显色更简单的铅笔画,如素描。paint你需要知道每一个像素的颜色,而Draw
并不用知道,只管用规定的颜色化就可以了。这就是为什么Draw
比Paint
更快的原因————“不用根据样式条件再去计算每个像素的颜色”。
body
的滚动与DOM
节点内的滚动(如一个div
的内容溢出产生滚动)稍有不同,DOM
节点上的滚动,都会产生两个GrahpicsLayer
,一个用于存放容器的层,一个用于存放滚动内容。body
滚动只会产生一个GraphicsLayer
,但是不论是body
上的滚动还是DOM
节点上的滚动,结果是一致的:

通过performance记录我们发现scroll
的动作只会产生Update Layers Tree
和Composite Layers
的操作。这两个

在滚动的过程中没有产生任何Paint
,只有Update Layer Tree
和Composite Layers
,所以极大的提高了性能。
许多浏览器会在body滚动上进行优化,所以并不用担心其性能,几乎所有的浏览器,body滚动性能都比DOM节点上的滚动表现更好
所以本着好到用在刀刃上的原则,
GraphicsLayer
会用本身内容偏向稳定,而使用场景偏复杂的一些场景上。

container
是一张桌子,RenderObject
是桌子上的花纹,而RenderLayer
是摆在桌子上的牌,都是一个平面上的东西。所以同样都是z-index
为0,RenderLayer
有着比普通RenderObject
更高的显示优先级,因为普通的RenderObject
是属于container
这一层的layer
,也就是最底层。
那是不是RenderObject
的显示优先级永远也无法比RenderLayer
高了呢?不是这样的,之前提到过z-index:0
的这个概念,对于有position概念的RebderLayer
,你可以将他的z-index
设置为-1

台子的花纹全都到上面来了,相当于放到了台板的背面。但是非position
类型的RenderLayer
是无法做到这一点的。
z-index
对于RenderLayer
主要影响在于重叠,而重叠的主要后果在于两个:RenderLayer
的合并以及升级。
之前的对照表中详细说明了RenderLayer
和GraphicsLayer
的形成原因,其中,如果一个带有position:relative,absolute
的RenderLayer
如果覆盖在一个GraphicsLayer
之上,这个RenderLayer
就会被升级为GraphicsLayer
,升级实际上是一个非常花费资源的操作,比如在做动画的时候,从RenderLayer
升级到GraphicsLayer
会对动画执行速度产生延时,请看例子:
以下每个绿色的圆形都是一个position:relative
的RenderLayer
,红色区域是一个position:fixed
的GraphicsLayer

上图中第一个图层3D模型中可以看到一共有4个GraphicsLayer
#document(292 x 2100)
.fixed(292 x 150)
.r(50 x 50)
.r(50 x 100)
#document
是根层,.fixed
是一个position:fixed
的层,.r(50 x 50)
是opacity:0.5
的层,.r(50 x 150)
是一个3个.r(50 x 50)
合并而来;.r
都是因为覆盖在.fixed
之上而形成的GraphicsLayer
,所以其他没有覆盖其上的'.r'都没有形成对应的GraphicsLayer
在滚动的过程中,由于.fixed
的位置固定,会经历许多.r
从.fixed
的上方经过的过程,按照GraphicsLayer
形成原理会多次形成GraphicsLayer
;以下描述了滚动中出现的三种情况:

- case1:之前已经描述过,这里不再累述
- case2:是向上滚动,原本未覆盖的
RenderLayer
进入了.fixed
的上方,所以会触发Update Layer Tree
,然后触发三次Paint
,最后触发Composited Layers
;我们来查看一下performance
:

这里可以看到三个paint
:
Location (0, -51); Dimemsions (292 x 2100); Layer Root #document; | Location (0, -1); Dimemsions (50 x 50); Layer Root div.r | Location (0, -51); Dimemsions (50 x 150); Layer Root div.r |
当第一个.r
完全移出.fixed
的范围之后,又会出现3次Paint
,主要主要是因为,原本单独的 .r
层因为不在.fixed
之上的范围,所以重新被合入到#document
之中,而原本的.r (50 x 150)
又会分离出一个.r (50 x 50)
和.r (50 x 100)
两个层,所以一共有3个GraphicsLayer
的内容产生了改变,所以产生了3次Paint
:

Location (0, -100); Dimemsions (292 x 2100); Layer Root #document; | Location (0, 0); Dimemsions (50 x 50); Layer Root div.r | Location (0, -51); Dimemsions (50 x 100); Layer Root div.r |
之前曾经说过,Paint
由于需要计算每个像素的颜色,所以非常消耗资源,而在滚动中快速触发这种Update Layer Tree
、Paint
、Paint
、Paint
、Compsite Layers
这种过程造成的性能消耗也是可想而知(有时会出现合并层的来不及显示的过程),如下图:

答案非常简单,可以将.fixed
的node节点置于.r
之后,或者直接提升或'.fixed'的z-index
属性,两个方案的实质上都是提升了z-index
;只要让覆盖在一个GraphicsLayer
之上的条件失效就可以了。
并不是每一个GraphicsLayer都是独立的,为了减少多次Paint
所带来的消耗GraphicsLayer
之间也会有合并。
以下所提到的合并和独立类型并不完整,欢迎大家补充。

第一个会单独形成一个GraphicsLayer
,其余同种类型会合成一个GraphicsLayer
。
relative/opacity混合效果也是一样的

独立型(各自为营)型 fixed
/transform
/animation
/relection
/will-change:transform,opacity
/overflow:scroll
/canvas
/video
:

scroll
与其他的独立层方式不同,内容移出产生滚动,会产生两个独立层;

chrome source关于squashDisallowed的更为详细的解释原文:SquashingDisallowedReasons.cpp
will-change 是chrome59以上的一个功能,作用是会给一个未来有个能做animation/transform/opacity变化的元素生成一个单独的
GraphicsLayer
,以免在动画开始的时候计算分离出单独的GraphicsLayer
,这样会产生延迟。
答案是No,Absolutely not,其实大家看到,就浏览器本身实现也分成合并型和独立型两种,其目的就是在于更好的节省资源和更好的性能体验,在dom数量一致的情况下,出现多个GraphicsLayer
和只有一个GraphicsLayer
的性能比较:
内容部分
<body>
<div class="content">白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼</div>
<div class="content">白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼</div>
<div class="content">白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼</div>
<div class="content">白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼</div>
<div class="content">白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼</div>
<div class="content">白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼</div>
<div class="content">白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼</div>
<div class="content">白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼</div>
<div class="content">白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼</div>
<div class="content">白日依山尽,黄河入海流,欲穷千里目,更上一层楼;白日依山尽,黄河入海流,欲穷千里目, 更上一层楼;白日依山尽,黄河入海流,欲穷千里目,更上一层楼
</div>
</body>
数据对比
来看一下layer数量对性能的影响
Performance table | |
![]() |
![]() |
Layer Veiw | |
![]() |
![]() |
每个Paint
都意味着有一个GraphicsLayer
产生,否则只会有一个GraphicsLayer
————#document
,可以从性能对比中看到,GraphicsLayer
越多,Paint
的次数也越多,并且Composite Layers
的时间也就越长,对于首屏展现来说,是非常不利的。
了解层的运作原理对于前端有着非常重要意义,通过优化层的覆盖关系,了解层的合并原理,合理使用层可以增加首屏渲染速度以及提示高用户使用过程中的流畅程度,是每一个前端都必须要好好研究的。