什么是浏览器层爆炸?

5,389 阅读13分钟

导读

本文会介绍chrome浏览器渲染工作方向上的一些理论知识点。
我们在面试的时候经常会被问到一些理论性的问题,尤其前端开发是在浏览器方面的知识。比如:

1合成层的“层”与层叠上下文的“层”是一个东西吗?
2层爆炸、层压缩是什么?怎么减少层爆炸带来的危害?
3都说要减少回流、重绘,怎样利用硬件加速做到?

这些问题你目前有思路可以回答上来了么,如果没有,看完这篇文章就有了~

浏览器渲染

首先我们先简单了解一下浏览器渲染流水线上的一些工作环节,初次渲染时会经过以下几步

  1. 构建DOM树;
  2. 样式计算;
  3. 布局定位;
  4. 图层分层;
  5. 图层绘制;
  6. 合成显示;

在CSS属性改变时,重渲染会分为“回流”、“重绘”和“直接合成”三种情况,分别对应从“布局定位”/“图层绘制”/“合成显示”开始,再走一遍上面的流程。

元素的CSS具体发生什么改变,则决定属于上面哪种情况:

1重绘:修改了一些不影响布局的属性,比如颜色; chonghui.webp

2回流(又叫重排):元素位置、大小发生变化导致其他节点联动,需要重新计算布局; chongpai.webp

3直接合成:合成层的transform、opacity修改,只需要将多个图层再次合并,而后生成位图,最终展示到屏幕上; hecheng.webp

渲染中的层

上面提到了渲染过程中会发生“图层分层”。浏览器中的层分为两种:“渲染层”和“合成层(也叫复合层)”。很多文章中还会提到一个概念叫“图形层”,其实可以把它当作合成层看待。

先直观的感受一下“层”,打开浏览器开发者工具的layers:

image.png 可以看到AB元素都在最底下的图层中,元素C是单独的一层,元素D又是一层。

之前说过,浏览器中的层分两种,渲染层和合成层,在开发者工具看到的全部都是合成层。 那么,怎样生成一个渲染层,又怎样才能形成一个合成层呢?

    <style>
      body {
        margin: 0;
        padding: 0;
      }
      .box {
        width: 100px;
        height: 100px;
        background: rgba(240, 163, 163, 0.4);
        border: 1px solid pink;
        border-radius: 10px;
        text-align: center;
      }
      #b {
        position: absolute;
        top: 0;
        left: 80px;
        z-index: 2;
      }
      #c {
        position: absolute;
        top: 0;

        left: 160px;
        z-index: 3;
        transform: translateZ(0);
      }
      #d {
        position: absolute;
        top: 0;

        left: 240px;
        z-index: 4;
      }
      .description {
        font-size: 10px;
      }
    </style>
    <div id="a" class="box">A</div>
    <div id="b" class="box">
      B
      <div class="description">z-index:2</div>
    </div>
    <div id="c" class="box">
      C
      <div class="description">z-index:3</div>
      <div class="description">transform: translateZ(0)</div>
    </div>
    <div id="d" class="box">
      D
      <div class="description">z-index:4</div>
    </div>

在讲合成层之前,我们先了解另外一个概念——层叠上下文

层叠上下文

层叠上下文(概念) 听起来比较抽象,我们假定用户正面向(浏览器)视窗或网页,而 HTML 元素沿着其相对于用户的一条虚构的 z 轴排开,层叠上下文就是对这些 HTML 元素的一个三维构想。在CSS规范中,每个盒模型的位置是三维的,分别是平面画布上的X轴,Y轴以及表示层叠的Z轴。一般情况下,元素在页面上沿X轴Y轴平铺,我们察觉不到它们在Z轴上的层叠关系。而一旦元素发生堆叠,这时就能发现某个元素可能覆盖了另一个元素或者被另一个元素覆盖。如果一个元素含有层叠上下文,(也就是说它是层叠上下文元素),我们可以理解为这个元素在Z轴上就“高人一等”,最终表现就是它离屏幕观察者更近。

也可以理解为,层叠上下文是html中某些元素的一个特殊属性,这个属性决定了他在空间的上下位置,而这个位置会影响到他们的渲染顺序。

最大的层叠上下文就是由文档根元素——html形成的:它自身连同它的子元素就形成了一个最大的层叠上下文,也就是说,我们写的所有代码都是在根层叠上下文里的。

特性:

  • 层叠上下文可以包含在其他层叠上下文中,并且一起创建一个层叠上下文的层级。(每个有z-index数值的元素也会连同它的子元素一起,生成一个小的层叠上下文,这个小层叠上下文和父级一样,拥有多个平面。)
  • 每个层叠上下文都是自包含的:当一个元素的内容发生层叠后,该元素将被作为整体在父级层叠上下文中按顺序进行层叠。

只有明确指定了z-index的值(不是auto)的定位元素(定义了position且值非static)才会生产一个层叠上下文,在这个层叠上下文中,内部元素层级都在它之上,哪怕是负数。

上面这些东西讲的太抽象了,我们之间看下面一个例子:

image.png

这边有3个div,2个父元素,我用了粉红色的边框区分,3个子元素,用绿色的背景区分。他们的层级关系是这样的

  <!-- container1:absolute z-index: 4 -->
  <!-- --1号:absolute z-index: -1 -->
  <!-- --2号:absolute z-index: 2 -->
  <!-- container2:absolute z-index: 1 -->
  <!-- --3号:absolute z-index: 3 -->
  最后的顺序从下到上是:container2(3号)-container1(1号-2号)

这里需要关注几个点:
1 z-index大的元素一定会盖在小的元素上面吗? (答案不是的,可以看到zindex为3到元素被其余的盖住了,为什么? 因为上面所讲的层叠上下文特性,每个层叠上下文是自包含的,他的位置是受他的父级上下文影响的,因为3号的父级元素上下文等级低,所以即使给3号元素zindex设置9999也是无法盖过其余两个元素的)
2 给元素设置负数z-index值,他的层级会被父级覆盖么(可以看到案例中没有,但是可不可以这样做呢? 其实是可以的,利用层叠等级的规则就行,后面会讲)

    <style>
      body {
        overflow-wrap: break-word;
        text-align: center;
        font-size: 14px;
      }
      p {
        font-weight: 700;
        font-size: 20px;
        line-height: 20px;
        margin: 0px;
      }

      .absolute {
        position: absolute;
      }
      .pink_block {
        border: 2px dashed pink;
        background-color: #ffddddc0;
        width: 120px;
        height: 120px;
      }
      .green_block {
        width: 100px;
        height: 100px;
        border: 1px dashed green;
        background-color: #ccffccc0;
      }
    </style>
          <div class="absolute pink_block" id="contaner1" style="z-index: 4">
        <div
          class="green_block absolute"
          style="z-index: -1; top: 10px; left: 10px"
        >
          <p>1</p>
          position: absolute z-index: -1
        </div>
        <div
          class="green_block absolute"
          style="z-index: 2; top: 90px; left: 30px"
        >
          <p>2</p>
          position: absolute z-index: 2
        </div>
      </div>

      <div
        class="absolute pink_block"
        id="contaner1"
        style="top: 50px; left: 50px; z-index: 1"
        z-index:0
      >
        <div
          class="green_block absolute"
          style="z-index: 3; top: 10px; left: 10px"
        >
          <p>3</p>
          position: absolute z-idex: 3
        </div>
      </div>

层叠等级(规则)

  • 在同一个层叠上下文中,它描述定义的是该层叠上下文中的层叠上下文元素在Z轴上的上下顺序。
  • 在其他普通元素中,它描述定义的是这些普通元素在Z轴上的上下顺序。 image.png

父盖子案例 image.png

这边给子元素设置负数的zindex后,为什么父级可以盖住子级,这就是利用了上面所讲的层叠等级规则, 块级元素的层叠等级是高于定位并且zindex<0的。 但是这个案例我个人是感觉没有什么实用价值的(实际开发中应该不会这么用吧。。)但是对于解释和验证这个层叠等级的规则是很受用的。

      <div class="pink_block" id="contaner1" style="position: relative">
        <div
          class="green_block absolute"
          style="z-index: -3; top: 10px; left: 10px"
        >
          <p>zi</p>
        </div>
      </div>

渲染层

渲染层的概念跟“层叠上下文”密切相关,简单来说,拥有z-index属性的定位元素会生成一个层叠上下文,一个生成层叠上下文的元素就生成了一个渲染层。

还是沿用上面的例子,BCD三个元素都是拥有z-index属性的定位元素(绝对定位),所以他们三个都形成了一个渲染层,加上document根元素形成的,一共是四个渲染层。(再强调一下,在开发者工具中看不到渲染层。)

形成渲染层的条件也就是形成层叠上下文的条件,有这几种情况:(只包含了部分日常开发中常用到的,全部的情况可以查mdn文档)

  1. document 元素
  2. 拥有z-index属性的定位元素(position: relative|fixed|sticky|absolute)
  3. 弹性布局的子项(父元素display:flex|inline-flex),并且z-index不是auto时
  4. opacity非1的元素
  5. transform非none的元素
  6. filter非none的元素
  7. will-change = opacity | transform | filter
  8. 此外需要剪裁的元素也会形成一个渲染层,也就是overflow不是visible的元素

合成层

只有一些特殊的渲染层才会被提升为合成层,通常来说有这些情况:

  1. document根元素
  2. transform:3D变换:translate3d,translateZ;
  3. will-change:opacity | transform | filter
  4. 对 opacity | transform | fliter 应用了过渡和动画(transition/animation)
  5. video、canvas、iframe
  6. 可滚动溢出元素,scrollable overflow

可以看出,上面这些条件属于生成渲染层的“加强版”,也就是说形成合成层的条件要更苛刻。

还是用开头的例子,C元素就是命中条件1,使用了3D变换transform: translateZ(0),于是被提升到一个单独的合成层。

但是D元素没有命中上面任何一条规则,却也是一个单独的合成层。因为还有一种情况——隐式合成。

隐式合成

当出现一个合成层后,层级顺序高于它的堆叠元素就会发生隐式合成。

我们给C、D元素设置层级,z-index分别是3和4;又在C元素上使用3D变换,提升成了合成层。此时,层级高于它的D元素就发生了隐式合成,也变成了一个合成层。

隐式合成出现的根本原是,元素发生了堆叠,浏览器为了保证最后的展示效果,不得不把层级顺序更高的元素拎出来盖在已有合成层上面。

层爆炸与层压缩

当页面内容非常多,层级复杂的时候,低层级的渲染层在某一时刻提升为合成层,那么此时改产生了很多预期外的合成层,——页面中所有 z-index 高于它的节点全部被提升,这些合成层都是相当消耗内存和GPU的,这个现象就是层爆炸。

解决思路:
1 代码层面控制,在会形成合成层的元素增加一个大的z-index属性,人为干扰合成的排序,可以有效减少chrome创建不必要的合成层,提升渲染性能,移动端优化效果尤为明显。
2 部分浏览器层压缩机制,多个渲染层同一个合成层重叠时,会自动将他们压缩到一起,避免“层爆炸”带来的损耗。

可以看下面这个例子,这边其实我设置了很多隐式合成层( D图层),但是在chrome浏览器中打开时,发现他和c图层的合成层进行合并了。

image.png 具体可以看代码 image.png

    <style>
      body {
        margin: 0;
        padding: 0;
      }
      .box {
        width: 100px;
        height: 100px;
        background: rgba(240, 163, 163, 0.4);
        border: 1px solid pink;
        border-radius: 10px;
        text-align: center;
      }
      #b {
        position: absolute;
        top: 0;
        left: 80px;
        z-index: 2;
      }
      #c {
        position: absolute;
        top: 0;

        left: 160px;
        z-index: 3;
        transform: translateZ(0);
      }
      #cc {
        position: absolute;
        top: 0;

        left: 160px;
        z-index: 3;
        /* transform: translateZ(0); */
      }
      #d {
        position: absolute;
        top: 0;

        left: 240px;
        z-index: 4;
        /* transform: translateZ(0); */
      }
      .description {
        font-size: 10px;
      }
    </style>
      <body>
    <div id="a" class="box">A</div>
    <div id="b" class="box">
      B
      <div class="description">z-index:2</div>
    </div>
    <div id="c" class="box">
      C
      <div class="description">z-index:3</div>
      <div class="description">transform: translateZ(0)</div>
    </div>
    <div id="cc" class="box">
      Cc
      <div class="description">z-index:3</div>
    </div>
    <div id="d" class="box">
      D
      <div class="description">z-index:4</div>
    </div>
    <div id="d" class="box">
      D
      <div class="description">z-index:4</div>
    </div>
    <div id="d" class="box">
      D
      <div class="description">z-index:4</div>
    </div>
    cc 和 多个d 压缩到同一个合成层
    <script></script>
  </body>

当然,浏览器的自动的层压缩也不是万能的,有很多特定情况下,浏览器是无法进行层压缩的,比如设置里mask属性、video 元素等等,另外也和使用等浏览器有关(safari)

上面同样的代码,在safari浏览器中打开,情况就不一样了。

image.png 可以看到在safari中出现了层爆炸的情况,也就是说在safari中并没有层压缩的功能。

硬件加速

上面讲了这么多,在实际开发中有什么用呢?或者说,浏览器为什么要分层呢?答案是硬件加速。听起来很厉害,其实不过是给HTML元素加上某些CSS属性,比如3D变换,将其提升成一个合成层,独立渲染。

之所以叫硬件加速,就是因为合成层会交给GPU(显卡)去处理,在硬件层面上开外挂,比在主线程(CPU)上效率更高。

提升成合成层的元素发生回流、重绘都只影响这一层,渲染效率得到提升。

来看一个例子,使用animation改变B元素的宽度,通过开发者工具Layers中的“paint count”的可以看到页面绘制次数会一直在增加,能直观感受到页面发生了“重绘”。

image.png

image.png

    <style>
      .box {
        width: 100px;
        height: 100px;
        background: rgba(240, 163, 163, 0.4);
        border: 1px solid pink;
        border-radius: 10px;
        text-align: center;
      }
      #a {
      }
      #b {
        position: absolute;
        top: 50;
        left: 50;
        z-index: 2;
        /* transform: translateZ(0); */
        animation: width-change 5s infinite;
        /* will-change: transform; */
        /* will-change: width; */
      }
      @keyframes width-change {
        0% {
          transform: scaleX(1);
          /* width: 80px;
          height: 80px; */
        }
        100% {
          transform: scale(1.5);

          /* width: 120px;
          height: 120px; */
        }
      }

      .description {
        font-size: 10px;
      }
    </style>

    <div id="a" class="box">A</div>
    <div id="b" class="box">
      B
      <div class="description">animation:width-change</div>
    </div>

可以注意到,重绘是发生在整个图层#document上的,也就是整个页面都要重绘。 image.png

给B元素加上will-change:transform开启硬件加速,让他提升成一个合成层,此时只有b元素在重绘 image.png

宽度的变化,在页面绘制的过程中会不断的重排,但是我们上面提到还有中方式是直接合成,若此时我们变化的属性改为transform: scale(2); 会发现当前这个合成层,变成只绘制一次了。 image.png

有得必有失,开启硬件加速后的合成层会交给GPU处理,当图层过多时,将会占用大量内存,尤其在移动端会造成卡顿,让优化适得其反。正确使用硬件加速就是在渲染效率和性能损耗之间找到一个平衡点,让页面渲染迅速不白屏,又流畅丝滑。

优化渲染性能

上面讲到了,利用硬件加速,可以把需要重排/重绘的元素单独拎出来,减少绘制的面积,除此之外,提升渲染性能还有这几个常见的方法:

  1. 避免重排/重绘,直接进行合成,合成层的transform 和 opacity的修改都是直接进入合成阶段的;比如可以使用transform:translate代替left/top修改元素的位置;使用transform:scale代替宽度、高度的修改;
  2. 注意隐式合成,给合成层一个较大的z-index值,虽然大部分浏览器已经实现了层压缩的能力,但是依旧有无法处理的情况,最好的办法就是一开始就避免层爆炸;
  3. 减小合成层占用的内存,合成层的最大问题就是占用内存较多,而内存的占用和元素的尺寸是成正比的。如果要实现一个100X100的元素,可以给宽高都设置为10px,再使用transform:scale(10)放大10倍,这样占用的内存只有直接设置的1/100;

image.png

image.png

总结

  1. 硬件加速并不是前端专有的东西,它是一个很宽泛的计算机概念——把软件的工作交给特定的硬件,更高效的完成某项任务。对于前端来说,就是使用特定的CSS属性,把元素提升成合成层,交给GPU处理;
  2. 合成层中的“层”可以被认为是真正物理上的层,浏览器把它独立出来,单独拿给GPU处理,而层叠上下文的“层”则是指渲染层,更像是一个概念上的层,一个合成层可以包含多个渲染层;
  3. 层爆炸指的是大量元素意料之外被提升成合成层,即隐式合成;层压缩是浏览器对隐式合成的优化,chrome94版本之后在这方面做到比较完善了;
  4. 使用transform、opacity取代传统属性来实现一些动画,并把他们提升到一个单独的合成层,能跳过布局计算和重新绘制,直接合成,能避免不必要的回流、重绘;