无线性能优化:Composite_html/css_WEB
一个 Web 页面的展示,简单来说可以认为经历了以下下几个步骤。
当然,本文我们只来关注 Composite 部分。 浏览器渲染原理在讨论 Composite 之前,有必要先简单了解下一些浏览器(本文只是针对 Chrome 来说)的渲染原理,方便对之后一些概念的理解。更多详细的内容可以参阅 GPU Accelerated Compositing in Chrome 注:由于 Chrome 对 Blank 引擎某些实现的修改,某些我们之前熟知的类名有了变化,比如 RenderObject 变成了 LayoutObject,RenderLayer 变成了 PaintLayer。感兴趣的看以参阅 Slimming Paint 。 在浏览器中,页面内容是存储为由 Node 对象组成的树状结构,也就是 DOM 树。每一个 HTML element 元素都有一个 Node 对象与之对应,DOM 树的根节点永远都是 Document Node。这一点相信大家都很熟悉了,但其实,从 DOM 树到最后的渲染,需要进行一些转换映射。
从 Nodes 到 LayoutObjectsDOM 树中得每个 Node 节点都有一个对应的 LayoutObject 。LayoutObject 知道如何在屏幕上 paint Node 的内容。 从 LayoutObjects 到 PaintLayers一般来说,拥有相同的坐标空间的 LayoutObjects,属于同一个渲染层(PaintLayer)。PaintLayer 最初是用来实现 stacking contest(层叠上下文) ,以此来保证页面元素以正确的顺序合成(composite),这样才能正确的展示元素的重叠以及半透明元素等等。因此满足形成层叠上下文条件的 LayoutObject 一定会为其创建新的渲染层,当然还有其他的一些特殊情况,为一些特殊的 LayoutObjects 创建一个新的渲染层,比如 overflow != visible 的元素。根据创建 PaintLayer 的原因不同,可以将其分为常见的 3 类:
满足以上条件的 LayoutObject 会拥有独立的渲染层,而其他的 LayoutObject 则和其第一个拥有渲染层的父元素共用一个。 从 PaintLayers 到 GraphicsLayers某些特殊的渲染层会被认为是合成层(Compositing Layers),合成层拥有单独的 GraphicsLayer,而其他不是合成层的渲染层,则和其第一个拥有 GraphicsLayer 父层公用一个。 每个 GraphicsLayer 都有一个 GraphicsContext,GraphicsContext 会负责输出该层的位图,位图是存储在共享内存中,作为纹理上传到 GPU 中,最后由 GPU 将多个位图进行合成,然后 draw 到屏幕上,此时,我们的页面也就展现到了屏幕上。 渲染层提升为合成层的原因有一下几种: 注:渲染层提升为合成层有一个先决条件,该渲染层必须是 SelfPaintingLayer(基本可认为是上文介绍的 NormalPaintLayer)。以下所讨论的渲染层提升为合成层的情况都是在该渲染层为 SelfPaintingLayer 前提下的。
层压缩
基本上常见的一些合成层的提升原因如上所说,你会发现,由于重叠的原因,可能随随便便就会产生出大量合成层来,而每个合成层都要消耗 CPU 和内存资源,岂不是严重影响页面性能。这一点浏览器也考虑到了,因此就有了层压缩(Layer Squashing)的处理。如果多个渲染层同一个合成层重叠时,这些渲染层会被压缩到一个 GraphicsLayer 中,以防止由于重叠原因导致可能出现的“层爆炸”。具体可以看如下 demo 。一开始,蓝色方块由于 translateZ 提升为了合成层,其他的方块元素因为重叠的原因,被压缩了一起,大小就是包含这 3 个方块的矩形大小。
当我们 hover 绿色方块时,会给其设置 translateZ 属性,导致绿色方块也被提升为合成层,则剩下的两个被压缩到了一起,大小就缩小为包含这 2 个方块的矩形大小。
当然,浏览器的自动的层压缩也不是万能的,有很多特定情况下,浏览器是无法进行层压缩的,如下所示,而这些情况也是我们应该尽量避免的。(注:以下情况都是基于重叠原因而言)
Text behind the orange box. 在本例中,`#overlap-child` 同合成层重叠,如果进行压缩,会导致渲染顺序的改变,其父元素 `#ancestor` 的 mask 属性将失效,因此类似这种情况下,是无法进行层压缩的。目前常见的产生这种原因的情况有两种,一种是上述的祖先元素使用 mask 属性的情况,另一种是祖先元素使用 filter 属性的情况([demo](http://taobaofed.github.io/demo/performance-composite-demo/squash/squashingWouldBreakPaintOrder-filter.html))。
不会被压缩到 composited div 上 本例中 `.target` 同 合成层 `.composited` 重叠,但是由于 `.composited` 在一个 `overflow: hidden` 的容器中,导致 `.target` 和合成层有不同的裁剪容器,从而 `.target` 无法被压缩。
本例中,红色的 `.composited` 提升为了合成层,绿色的 `.overlap` fix 在页面顶部,一开始只有 `.composited` 合成层。  当滑动页面,`.overlap` 重叠到 `.composited` 上时,`.overlap` 会因重叠原因提升为合成层,同时,因为相对于合成层滚动,因此无法被压缩。 
如何查看合成层使用 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 来确认这么做是否真的改进了渲染性能。别盲目提升合成层,一定要分析其实际性能表现。 使用 transform 或者 opacity 来实现动画效果文章最开始,我们讲到了页面呈现出来所经历的渲染流水线,其实从性能方面考虑,最理想的渲染流水线是没有布局和绘制环节的,只需要做合成层的合并即可:
为了实现上述效果,就需要只使用那些仅触发 Composite 的属性。目前,只有两个属性是满足这个条件的:transforms 和 opacity。更详细的信息可以查看 CSS Triggers 。 注意:元素提升为合成层后,transform 和 opacity 才不会触发 paint,如果不是合成层,则其依然会触发 paint。具体见如下两个 demo。
可以看到未提升 target element 为合成层,transform 和 opacity 依然会触发 paint。 减少绘制区域对于不需要重新绘制的区域应尽量避免绘制,以减少绘制区域,比如一个 fix 在页面顶部的固定不变的导航 header,在页面内容某个区域 repaint 时,整个屏幕包括 fix 的 header 也会被重绘,见 demo ,结果如下:
而对于固定不变的区域,我们期望其并不会被重绘,因此可以通过之前的方法,将其提升为独立的合成层。 减少绘制区域,需要仔细分析页面,区分绘制区域,减少重绘区域甚至避免重绘。 合理管理合成层看完上面的文章,你会发现提升合成层会达到更好的性能。这看上去非常诱人,但是问题是,创建一个新的合成层并不是免费的,它得消耗额外的内存和管理资源。实际上,在内存资源有限的设备上,合成层带来的性能改善,可能远远赶不上过多合成层开销给页面性能带来的负面影响。同时,由于每个渲染层的纹理都需要上传到 GPU 处理,因此我们还需要考虑 CPU 和 GPU 之间的带宽问题、以及有多大内存供 GPU 处理这些纹理的问题。 对于合成层占用内存的问题,我们简单做了几个 demo 进行了验证。 demo 1 和 demo 2 中,会创建 2000 个同样的 div 元素,不同的是 demo 2 中的元素通过 will-change 都提升为了合成层,而两个 demo 页面的内存消耗却有很明显的差别。
防止层爆炸通过之前的介绍,我们知道同合成层重叠也会使元素提升为合成层,虽然有浏览器的层压缩机制,但是也有很多无法进行压缩的情况。也就是说除了我们显式的声明的合成层,还可能由于重叠原因不经意间产生一些不在预期的合成层,极端一点可能会产生大量的额外合成层,出现层爆炸的现象。我们简单写了一个极端点但其实在我们的页面中比较常见的 demo 。
composited animating
demo 中, .animating 的合成层在运行动画,会导致 .inner 元素因为上文介绍过的 assumedOverlap 的原因,而被提升为合成层,同时, .inner 的父元素 .box 设置了 overflow: hidden ,导致 .inner 的合成层因为 squashingClippingContainerMismatch 的原因,无法压缩,就出现了层爆炸的问题。
这种情况平时在我们的业务中还是很常见的,比如 slider + list 的结构,一旦满足了无法进行层压缩的情况,就很容易出现层爆炸的问题。 解决层爆炸的问题,最佳方案是打破 overlap 的条件,也就是说让其他元素不要和合成层元素重叠。对于上述的示例,我们可以将 .animation 的 z-index 提高。修改后 demo
.animating { ... /* 让其他元素不和合成层重叠 */ position: relative; z-index: 1;} 此时,就只有 .animating 提升为合成层,如下:
同时,内存占用比起之前也降低了很多。
如果受限于视觉需要等因素,其他元素必须要覆盖在合成层之上,那应该尽量避免无法层压缩情况的出现。针对上述示例中,无法层压缩的情况(squashingClippingContainerMismatch),我们可以将 .box 的 overflow: hidden 去掉,这样就可以利用浏览器的层压缩了。修改后 demo 此时,由于第一个 .box 因为 squashingLayerIsAnimating 的原因无法压缩,其他的都被压缩到了一起。
同时,内存占用比起之前也降低了很多。
最后之前无线开发时,大多数人都很喜欢使用 translateZ(0) 来进行所谓的硬件加速,以提升性能,但是性能优化并没有所谓的“银弹”, translateZ(0) 不是,本文列出的优化建议也不是。抛开了对页面的具体分析,任何的性能优化都是站不住脚的,盲目的使用一些优化措施,结果可能会适得其反。因此切实的去分析页面的实际性能表现,不断的改进测试,才是正确的优化途径。 参考
|