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

2022-07-29 20:50:24 浏览数 (1)

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

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

页面渲染的流程

首先来复习一下一个老八股:页面渲染的流程, 简单来说,初次渲染时会经过以下几步

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

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

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

  1. 回流(又叫重排):元素位置、大小发生变化导致其他节点联动,需要重新计算布局;
  2. 重绘:修改了一些不影响布局的属性,比如颜色;
  3. 直接合成:合成层的transform、opacity修改,只需要将多个图层再次合并,而后生成位图,最终展示到屏幕上;

渲染中的层

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

开发者工具中的Layers

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

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

代码语言:javascript复制
<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根元素形成的,一共是四个渲染层。(再强调一下,在开发者工具中看不到渲染层。)

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

  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

此外需要剪裁的元素也会形成一个渲染层,也就是overflow不是visible的元素

合成层

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

  1. transform:3D变换:translate3d,translateZ;
  2. will-change:opacity | transform | filter
  3. 对 opacity | transform | fliter 应用了过渡和动画(transition/animation)
  4. 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上的,也就是整个页面都要重绘。

代码语言:javascript复制
<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图层的绘制次数不会一直增加了。

代码语言:javascript复制
<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处理,当图层过多时,将会占用大量内存,尤其在移动端会造成卡顿,让优化适得其反。正确使用硬件加速就是在渲染效率和性能损耗之间找到一个平衡点,让页面渲染迅速不白屏,又流畅丝滑。

优化渲染性能

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

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

结语

回到开头的几个问题,答案不难在文中找到:

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

如果觉得这篇文章对你有帮助,给我点个赞和在看呗~

你的支持是我创作的最大动力!❤️

0 人点赞