本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
大家好,我是年年!这篇文章是关于浏览器渲染中 “分层” 与硬件加速的,我会讲清 :
什么是硬件加速?
合成层的 “层” 与层叠上下文的 “层” 是一个东西吗?
层爆炸、层压缩是什么?
都说要减少回流、重绘,怎样利用硬件加速做到?
页面渲染的流程
首先来复习一下一个老八股:页面渲染的流程, 简单来说,初次渲染时会经过以下几步
构建 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 取代传统属性来实现一些动画,并把他们提升到一个单独的合成层,能跳过布局计算和重新绘制,直接合成,能避免不必要的回流、重绘;
如果觉得这篇文章对你有帮助,给我点个赞和在看呗~
你的支持是我创作的最大动力!❤️