Skip to content

探究"层"的使用

SeedHuang edited this page Jul 28, 2017 · 1 revision

探究“层”的使用

作者:黄春华

Why?

前端每天都在和渲染打交道,比如改变大小,改变颜色,或者插入一个新节点,都会促使屏幕上的显示内容发生变化,而这些变化都会体现在层上。优化层的使用作为是主要的性能优化点之一,主要工作就是通过了解层的产生与合并的原因进行合理优化,所以了解层的运作机制就好比读懂武器说明一样重要。

What's Next?

在了解什么是层之前,我们先来了解一下,从Html Parse->GraphicsLayer Tree在这些操作过程中到底都发生了一些什么?

从DOM到GraphicsLayer Tree

这是一个复杂的过程:下图简单的讲述了这个过程。

Layer的形成条件

从上文看见整个形成过程中,只有两种层,一种是RenderLayer(负责DOM子树),一种是GraphicsLayer(负责RenderLayer子树),对两者形成的条件进行比较

RenderLayer GraphicsLayer
页面元素的根目录 #document
RenderObject具有position 样式属性的。 RenderLayer覆盖在一个同级GraphicsLayer之上,RenderLayerz-index大于GraphicsLayer
RenderObject有透明效果 RenderLayer使用CSS动画、opacity< 1
RenderObject具有overflow,alpha或者反射效果的节点 RenderLayer具有Reflection样式属性。
RenderObject使用Canvas2D和3D(WebGL)技术的RenderObject节点 RenderLayercanvas,并满足三个条件
RenderObjectvideo节点 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

为什么要有RenderLayer和GraphicsLayer

可以看的出,GraphicsLayerRenderLayer定义的更加严谨,在满足一定条件的情况下RenderLayer可以转换成GraphicsLayer,为什么要有RenderLayerGraphicsLayer,本身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来说合并图像比重绘图像要高效的多

简述渲染的4个过程

  • 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 vs Paint

DrawPaint。这两字很容易混淆,首先字面理解,Paint对应的彩色的绘画,如油彩画,而draw对应的是显色更简单的铅笔画,如素描。paint你需要知道每一个像素的颜色,而Draw并不用知道,只管用规定的颜色化就可以了。这就是为什么DrawPaint更快的原因————“不用根据样式条件再去计算每个像素的颜色”。

如果说有什么最能体现Draw性能上优越性,最好的例子就是滚动:

body的滚动与DOM节点内的滚动(如一个div的内容溢出产生滚动)稍有不同,DOM节点上的滚动,都会产生两个GrahpicsLayer,一个用于存放容器的层,一个用于存放滚动内容。body滚动只会产生一个GraphicsLayer,但是不论是body上的滚动还是DOM节点上的滚动,结果是一致的:

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

在滚动的过程中没有产生任何Paint,只有Update Layer TreeComposite Layers,所以极大的提高了性能。

许多浏览器会在body滚动上进行优化,所以并不用担心其性能,几乎所有的浏览器,body滚动性能都比DOM节点上的滚动表现更好

所以本着好到用在刀刃上的原则,GraphicsLayer会用本身内容偏向稳定,而使用场景偏复杂的一些场景上。

层的3维空间

同一平面上的层

container是一张桌子,RenderObject是桌子上的花纹,而RenderLayer是摆在桌子上的牌,都是一个平面上的东西。所以同样都是z-index为0,RenderLayer有着比普通RenderObject更高的显示优先级,因为普通的RenderObject是属于container这一层的layer,也就是最底层。

z-index

那是不是RenderObject的显示优先级永远也无法比RenderLayer高了呢?不是这样的,之前提到过z-index:0的这个概念,对于有position概念的RebderLayer,你可以将他的z-index设置为-1

台子的花纹全都到上面来了,相当于放到了台板的背面。但是非position类型的RenderLayer是无法做到这一点的。

重叠

z-index对于RenderLayer主要影响在于重叠,而重叠的主要后果在于两个:RenderLayer的合并以及升级。

RenderLayer升级GraphicsLayer的策略。

之前的对照表中详细说明了RenderLayerGraphicsLayer的形成原因,其中,如果一个带有position:relative,absoluteRenderLayer如果覆盖在一个GraphicsLayer之上,这个RenderLayer就会被升级为GraphicsLayer,升级实际上是一个非常花费资源的操作,比如在做动画的时候,从RenderLayer升级到GraphicsLayer会对动画执行速度产生延时,请看例子: 以下每个绿色的圆形都是一个position:relativeRenderLayer,红色区域是一个position:fixedGraphicsLayer

上图中第一个图层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 TreePaintPaintPaintCompsite Layers这种过程造成的性能消耗也是可想而知(有时会出现合并层的来不及显示的过程),如下图:

何解决这个问题?

答案非常简单,可以将.fixed的node节点置于.r之后,或者直接提升或'.fixed'的z-index属性,两个方案的实质上都是提升了z-index;只要让覆盖在一个GraphicsLayer之上的条件失效就可以了。

GraphicsLayer的(Squashing)合并与(SquashingDisallowed)独立

并不是每一个GraphicsLayer都是独立的,为了减少多次Paint所带来的消耗GraphicsLayer之间也会有合并。

以下所提到的合并和独立类型并不完整,欢迎大家补充。

合并类型(relative/absoluste/opacity/mask/transform2d):

第一个会单独形成一个GraphicsLayer,其余同种类型会合成一个GraphicsLayer

relative/opacity混合效果也是一样的

独立型(各自为营)型 fixedtransformanimationrelectionwill-change:transform,opacity/overflow:scrollcanvasvideo:

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

chrome source关于squashDisallowed的更为详细的解释原文:SquashingDisallowedReasons.cpp

will-change 是chrome59以上的一个功能,作用是会给一个未来有个能做animation/transform/opacity变化的元素生成一个单独的GraphicsLayer,以免在动画开始的时候计算分离出单独的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的时间也就越长,对于首屏展现来说,是非常不利的。

总结

了解层的运作原理对于前端有着非常重要意义,通过优化层的覆盖关系,了解层的合并原理,合理使用层可以增加首屏渲染速度以及提示高用户使用过程中的流畅程度,是每一个前端都必须要好好研究的。

参考