本文共 9929 字,大约阅读时间需要 33 分钟。
本文最初发表在
一个 Web 页面的展示,简单来说可以认为经历了以下下几个步骤。
<body>
元素的宽度的变化会影响其子元素的宽度,其子元素宽度的变化也会继续对其孙子元素产生影响。因此对于浏览器来说,布局过程是经常发生的。当然,本文我们只来关注 Composite 部分。
在讨论 Composite 之前,有必要先简单了解下一些浏览器(本文只是针对 Chrome 来说)的渲染原理,方便对之后一些概念的理解。更多详细的内容可以参阅
注:由于 Chrome 对 Blank 引擎某些实现的修改,某些我们之前熟知的类名有了变化,比如 RenderObject 变成了 LayoutObject,RenderLayer 变成了 PaintLayer。感兴趣的看以参阅 。
在浏览器中,页面内容是存储为由 Node 对象组成的树状结构,也就是 DOM 树。每一个 HTML element 元素都有一个 Node 对象与之对应,DOM 树的根节点永远都是 Document Node。这一点相信大家都很熟悉了,但其实,从 DOM 树到最后的渲染,需要进行一些转换映射。
DOM 树中得每个 Node 节点都有一个对应的 LayoutObject 。LayoutObject 知道如何在屏幕上 paint Node 的内容。
一般来说,拥有相同的坐标空间的 LayoutObjects,属于同一个渲染层(PaintLayer)。PaintLayer 最初是用来实现 ,以此来保证页面元素以正确的顺序合成(composite),这样才能正确的展示元素的重叠以及半透明元素等等。因此满足形成层叠上下文条件的 LayoutObject 一定会为其创建新的渲染层,当然还有其他的一些特殊情况,为一些特殊的 LayoutObjects 创建一个新的渲染层,比如 overflow != visible
的元素。根据创建 PaintLayer 的原因不同,可以将其分为常见的 3 类:
NormalPaintLayer
OverflowClipPaintLayer
NoPaintLayer
满足以上条件的 LayoutObject 会拥有独立的渲染层,而其他的 LayoutObject 则和其第一个拥有渲染层的父元素共用一个。
某些特殊的渲染层会被认为是合成层(Compositing Layers),合成层拥有单独的 GraphicsLayer,而其他不是合成层的渲染层,则和其第一个拥有 GraphicsLayer 父层公用一个。
每个 GraphicsLayer 都有一个 GraphicsContext,GraphicsContext 会负责输出该层的位图,位图是存储在共享内存中,作为纹理上传到 GPU 中,最后由 GPU 将多个位图进行合成,然后 draw 到屏幕上,此时,我们的页面也就展现到了屏幕上。
渲染层提升为合成层的原因有一下几种:
注:渲染层提升为合成层有一个先决条件,该渲染层必须是 SelfPaintingLayer(基本可认为是上文介绍的 NormalPaintLayer)。以下所讨论的渲染层提升为合成层的情况都是在该渲染层为 SelfPaintingLayer 前提下的。
直接原因(direct reason)
3D 或者 硬件加速的 2D Canvas 元素
对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition(需要是 active 的 animation 或者 transition,当 animation 或者 transition 效果未开始或结束后,提升合成层也会失效)
后代元素原因
overlap 重叠原因
为什么会因为重叠原因而产生合成层呢?举个简单的栗子。
蓝色的矩形重叠在绿色矩形之上,同时它们的父元素是一个 GraphicsLayer。此时假设绿色矩形为一个 GraphicsLayer,如果 overlap 无法提升合成层的话,那么蓝色矩形不会提升为合成层,也就会和父元素公用一个 GraphicsLayer。
此时,渲染顺序就会发生错误,因此为保证渲染顺序,overlap 也成为了合成层产生的原因,也就是如下的正常情形。
当然 overlap 的原因也会细分为几类,接下来我们会详细看下。
重叠或者说部分重叠在一个合成层之上。
那如何算是重叠呢,最常见和容易理解的就是元素的 border box(content + padding + border) 和合成层的有重叠,比如:,当然 margin area 的重叠是无效的()。其他的还有一些不常见的情况,也算是同合成层重叠的条件,如下:
overflow:auto
还是 overflow:scrill
,只要是能 scroll 即可) 的元素同一个合成层重叠,则其可视子元素也同该合成层重叠 这个原因听上去有点虚,什么叫假设重叠?其实也比较好理解,比如一个元素的 CSS 动画效果,动画运行期间,元素是有可能和其他元素有重叠的。针对于这种情况,于是就有了 assumedOverlap 的合成层产生原因,示例可见:。在本 demo 中,动画元素视觉上并没有和其兄弟元素重叠,但因为 assumedOverlap 的原因,其兄弟元素依然提升为了合成层。
需要注意的是该原因下,有一个很特殊的情况:
如果合成层有内联的 transform 属性,会导致其兄弟渲染层 assume overlap,从而提升为合成层。比如:。
基本上常见的一些合成层的提升原因如上所说,你会发现,由于重叠的原因,可能随随便便就会产生出大量合成层来,而每个合成层都要消耗 CPU 和内存资源,岂不是严重影响页面性能。这一点浏览器也考虑到了,因此就有了层压缩(Layer Squashing)的处理。如果多个渲染层同一个合成层重叠时,这些渲染层会被压缩到一个 GraphicsLayer 中,以防止由于重叠原因导致可能出现的“层爆炸”。具体可以看如下 。一开始,蓝色方块由于
translateZ
提升为了合成层,其他的方块元素因为重叠的原因,被压缩了一起,大小就是包含这 3 个方块的矩形大小。 当我们 hover 绿色方块时,会给其设置 translateZ
属性,导致绿色方块也被提升为合成层,则剩下的两个被压缩到了一起,大小就缩小为包含这 2 个方块的矩形大小。
当然,浏览器的自动的层压缩也不是万能的,有很多特定情况下,浏览器是无法进行层压缩的,如下所示,而这些情况也是我们应该尽量避免的。(注:以下情况都是基于重叠原因而言)
无法进行会打破渲染顺序的压缩(squashingWouldBreakPaintOrder)
示例如下:[demo](http://taobaofed.github.io/demo/performance-composite-demo/squash/squashingWouldBreakPaintOrder-mask.html)
Text behind the orange box.
在本例中,`#overlap-child` 同合成层重叠,如果进行压缩,会导致渲染顺序的改变,其父元素 `#ancestor` 的 mask 属性将失效,因此类似这种情况下,是无法进行层压缩的。目前常见的产生这种原因的情况有两种,一种是上述的祖先元素使用 mask 属性的情况,另一种是祖先元素使用 filter 属性的情况([demo](http://taobaofed.github.io/demo/performance-composite-demo/squash/squashingWouldBreakPaintOrder-filter.html))。
当渲染层同合成层有不同的裁剪容器(clipping container)时,该渲染层无法压缩(squashingClippingContainerMismatch)。
示例如下:[demo](http://taobaofed.github.io/demo/performance-composite-demo/squash/squashingClippingContainerMismatch.html)
不会被压缩到 composited div 上
本例中 `.target` 同 合成层 `.composited` 重叠,但是由于 `.composited` 在一个 `overflow: hidden` 的容器中,导致 `.target` 和合成层有不同的裁剪容器,从而 `.target` 无法被压缩。
相对于合成层滚动的渲染层无法被压缩(scrollsWithRespectToSquashingLayer)
示例如下:[demo](http://taobaofed.github.io/demo/performance-composite-demo/squash/scrollsWithRespectToSquashingLayer.html)
本例中,红色的 `.composited` 提升为了合成层,绿色的 `.overlap` fix 在页面顶部,一开始只有 `.composited` 合成层。![](https://img.alicdn.com/tps/TB1SHBOMXXXXXbnXFXXXXXXXXXX-690-484.jpg_640x640.jpg)当滑动页面,`.overlap` 重叠到 `.composited` 上时,`.overlap` 会因重叠原因提升为合成层,同时,因为相对于合成层滚动,因此无法被压缩。![](https://img.alicdn.com/tps/TB1IrRGMXXXXXXxaXXXXXXXXXXX-690-484.jpg_640x640.jpg)
当覆盖的合成层正在运行动画时,该渲染层无法压缩(squashingLayerIsAnimating),当动画未开始或者运行完毕以后,该渲染层才可以被压缩
![](https://img.alicdn.com/tps/TB1mJjwMXXXXXajXpXXXXXXXXXX-960-391.jpg)
使用 Chrome DevTools 工具来查看页面中合成层的情况。
比较简单的方法是打开 DevTools,勾选上 Show layer borders
其中,页面上的合成层会用黄色边框框出来。
当然,更加详细的信息可以通过 Timeline 来查看。
每一个单独的帧,看到每个帧的渲染细节:
点击之后,你就会在视图中看到一个新的选项卡:Layers。
点击这个 Layers 选项卡,你会看到一个新的视图。在这个视图中,你可以对这一帧中的所有合成层进行扫描、缩放等操作,同时还能看到每个渲染层被创建的原因。
有了这个视图,你就能知道页面中到底有多少个合成层。如果你在对页面滚动或渐变效果的性能分析中发现 Composite 过程耗费了太多时间,那么你可以从这个视图里看到页面中有多少个渲染层,它们为何被创建,从而对合成层的数量进行优化。
提升为合成层简单说来有以下几点好处:
利用合成层对于提升页面性能方面有很大的作用,因此我们也总结了一下几点优化建议。
合成层的好处是不会影响到其他元素的绘制,因此,为了减少动画元素对其他元素的影响,从而减少 paint,我们需要把动画效果中的元素提升为合成层。
提升合成层的最好方式是使用 CSS 的 will-change 属性。从上一节合成层产生原因中,可以知道 will-change 设置为 opacity、transform、top、left、bottom、right 可以将元素提升为合成层。
#target { will-change: transform;}
其兼容如下所示:
对于那些目前还不支持 will-change 属性的浏览器,目前常用的是使用一个 3D transform 属性来强制提升为合成层:
#target { transform: translateZ(0);}
但需要注意的是,不要创建太多的渲染层。因为每创建一个新的渲染层,就意味着新的内存分配和更复杂的层的管理。之后我们会详细讨论。
如果你已经把一个元素放到一个新的合成层里,那么可以使用 Timeline 来确认这么做是否真的改进了渲染性能。别盲目提升合成层,一定要分析其实际性能表现。
文章最开始,我们讲到了页面呈现出来所经历的渲染流水线,其实从性能方面考虑,最理想的渲染流水线是没有布局和绘制环节的,只需要做合成层的合并即可:
为了实现上述效果,就需要只使用那些仅触发 Composite 的属性。目前,只有两个属性是满足这个条件的:transforms 和 opacity。更详细的信息可以查看 。
注意:元素提升为合成层后,transform 和 opacity 才不会触发 paint,如果不是合成层,则其依然会触发 paint。具体见如下两个 demo。
![no-promote-transform](https://img.alicdn.com/tps/TB1XsAVLpXXXXbzXpXXXXXXXXXX-712-411.png_640x640.jpg)
![no-promote-opacity](https://img.alicdn.com/tps/TB1j9M7LpXXXXaiXXXXXXXXXXXX-713-424.png_640x640.jpg)
可以看到未提升 target element 为合成层,transform 和 opacity 依然会触发 paint。
对于不需要重新绘制的区域应尽量避免绘制,以减少绘制区域,比如一个 fix 在页面顶部的固定不变的导航 header,在页面内容某个区域 repaint 时,整个屏幕包括 fix 的 header 也会被重绘,见 ,结果如下:
而对于固定不变的区域,我们期望其并不会被重绘,因此可以通过之前的方法,将其提升为独立的合成层。
减少绘制区域,需要仔细分析页面,区分绘制区域,减少重绘区域甚至避免重绘。
看完上面的文章,你会发现提升合成层会达到更好的性能。这看上去非常诱人,但是问题是,创建一个新的合成层并不是免费的,它得消耗额外的内存和管理资源。实际上,在内存资源有限的设备上,合成层带来的性能改善,可能远远赶不上过多合成层开销给页面性能带来的负面影响。同时,由于每个渲染层的纹理都需要上传到 GPU 处理,因此我们还需要考虑 CPU 和 GPU 之间的带宽问题、以及有多大内存供 GPU 处理这些纹理的问题。
对于合成层占用内存的问题,我们简单做了几个 demo 进行了验证。
和 中,会创建 2000 个同样的 div 元素,不同的是 中的元素通过 will-change 都提升为了合成层,而两个 demo 页面的内存消耗却有很明显的差别。
通过之前的介绍,我们知道同合成层重叠也会使元素提升为合成层,虽然有浏览器的层压缩机制,但是也有很多无法进行压缩的情况。也就是说除了我们显式的声明的合成层,还可能由于重叠原因不经意间产生一些不在预期的合成层,极端一点可能会产生大量的额外合成层,出现层爆炸的现象。我们简单写了一个极端点但其实在我们的页面中比较常见的 。
composited animating
asume overlap, 因为 squashingClippingContainerMismatch 无法压缩
中,.animating
的合成层在运行动画,会导致 .inner
元素因为上文介绍过的 assumedOverlap 的原因,而被提升为合成层,同时,.inner
的父元素 .box
设置了 overflow: hidden
,导致 .inner
的合成层因为 squashingClippingContainerMismatch 的原因,无法压缩,就出现了层爆炸的问题。
这种情况平时在我们的业务中还是很常见的,比如 slider + list 的结构,一旦满足了无法进行层压缩的情况,就很容易出现层爆炸的问题。
解决层爆炸的问题,最佳方案是打破 overlap 的条件,也就是说让其他元素不要和合成层元素重叠。对于上述的示例,我们可以将 .animation
的 z-index 提高。修改后
.animating { ... /* 让其他元素不和合成层重叠 */ position: relative; z-index: 1;}
此时,就只有 .animating
提升为合成层,如下:
同时,内存占用比起之前也降低了很多。
如果受限于视觉需要等因素,其他元素必须要覆盖在合成层之上,那应该尽量避免无法层压缩情况的出现。针对上述示例中,无法层压缩的情况(squashingClippingContainerMismatch),我们可以将 .box
的 overflow: hidden
去掉,这样就可以利用浏览器的层压缩了。修改后
此时,由于第一个 .box
因为 squashingLayerIsAnimating 的原因无法压缩,其他的都被压缩到了一起。
同时,内存占用比起之前也降低了很多。
之前无线开发时,大多数人都很喜欢使用 translateZ(0)
来进行所谓的硬件加速,以提升性能,但是性能优化并没有所谓的“银弹”,translateZ(0)
不是,本文列出的优化建议也不是。抛开了对页面的具体分析,任何的性能优化都是站不住脚的,盲目的使用一些优化措施,结果可能会适得其反。因此切实的去分析页面的实际性能表现,不断的改进测试,才是正确的优化途径。
转载地址:http://ygwsl.baihongyu.com/