抖音三面:硬件加速中的“层”和层叠上下文中的“层”,是一个东西吗?


大家好,我是年年!这篇文章是关于浏览器渲染中“分层”与硬件加速的,我会讲清 :

什么是硬件加速?
合成层的“层”与层叠上下文的“层”是一个东西吗?
层爆炸、层压缩是什么?
都说要减少回流、重绘,怎样利用硬件加速做到?
页面渲染的流程
首先来复习一下一个老八股:页面渲染的流程, 简单来说,初次渲染时会经过以下几步

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

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

回流(又叫重排):元素位置、大小发生变化导致其他节点联动,需要重新计算布局;
重绘:修改了一些不影响布局的属性,比如颜色;
直接合成:合成层的transform、opacity修改,只需要将多个图层再次合并,而后生成位图,最终展示到屏幕上;
渲染中的层
上面提到了渲染过程中会发生“图层分层”。浏览器中的层分为两种:“渲染层”和“合成层(也叫复合层)”。很多文章中还会提到一个概念叫“图形层”,其实可以把它当作合成层看待。为了降低理解成本,本文全部使用“渲染层”和“合成层”这两个名词描述。

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


可以看到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;
  }
  #a {
  }
  #b {
    position: absolute;
    top:0;
    left: 80;
    z-index: 2;
  }
  #c {
    position: absolute;
    top:0;

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

    left: 240;
    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>
之前说过,浏览器中的层分两种,渲染层和合成层,在这里看到的全部都是合成层。那么,怎样生成一个渲染层,又怎样才能形成一个合成层呢?

渲染层
渲染层的概念跟“层叠上下文”密切相关,之前也写过一篇文章,可以看这里。简单来说,拥有z-index属性的定位元素会生成一个层叠上下文,一个生成层叠上下文的元素就生成了一个渲染层。

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

形成渲染层的条件也就是形成层叠上下文的条件,有这几种情况:

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






合成层
在开发者工具中看到的不是渲染层,而是下面要讲的合成层,只有一些特殊的渲染层才会被提升为合成层,通常来说有这些情况:

transform:3D变换:translate3d,translateZ;
will-change:opacity | transform | filter
对 opacity | transform | fliter 应用了过渡和动画(transition/animation)
video、canvas、iframe
可以看出,上面这些条件属于生成渲染层的“加强版”,也就是说形成合成层的条件要更苛刻。

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

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

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

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

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

层爆炸与层压缩
这是我在项目中实际遇到的一个问题:一个页面在低端机器上滚动时非常卡顿,排查了很久,最后发现原因就在于隐式合成带来的层爆炸。

隐式合成产生了很多预期外的合成层——页面中所有 z-index 高于它的节点全部被提升,这些合成层都是相当消耗内存和GPU的。所以带给我们的启示是给合成层一个大的z-index值,避免出现隐式合成。

还好浏览器逐渐进行了优化,也就是层压缩机制——多个渲染层同一个合成层重叠时,会自动将他们压缩到一起,避免“层爆炸”带来的损耗。

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

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

就像在ipad上画画一样,画手都是在不同的图层绘制线稿、上色,这样才方便后期修改,不至于牵一发而动全身。提升成合成层的元素发生回流、重绘都只影响这一层,渲染效率得到提升。

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


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

<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;
    animation: width-change 5s infinite;
  }
  @keyframes width-change {
    0% {
      width: 80px;
    }
    100% {
      width: 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>
给B元素加上will-change:transform开启硬件加速,让他提升成一个合成层。


会发现重绘只发生在这个图层上,#document图层的绘制次数不会一直增加了。

<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;
    animation: width-change 5s infinite;
    will-change: transform;
  }
  @keyframes width-change {
    0% {
      width: 80px;
    }
    100% {
      width: 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>

这就是硬件加速的意义:我们在讲到性能优化时,经常会说减少回流、重绘,如果能直接避免当然是最好,但如果实在没法避免,可以使用硬件加速,让这个元素单独回流、重绘,减少绘制的面积。

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






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

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

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




作者:年年


欢迎关注微信公众号 :深圳湾码农