无线性能优化:Composite

623 查看

无线性能优化:Composite

一个 Web 页面的展示,简单来说可以认为经历了以下下几个步骤。

  • JavaScript:一般来说,我们会使用 JavaScript 来实现一些视觉变化的效果。比如做一个动画或者往页面里添加一些 DOM 元素等。
  • Style:计算样式,这个过程是根据 CSS 选择器,对每个 DOM 元素匹配对应的 CSS 样式。这一步结束之后,就确定了每个 DOM 元素上该应用什么 CSS 样式规则。
  • Layout:布局,上一步确定了每个 DOM 元素的样式规则,这一步就是具体计算每个 DOM 元素最终在屏幕上显示的大小和位置。web 页面中元素的布局是相对的,因此一个元素的布局发生变化,会联动地引发其他元素的布局发生变化。比如, 元素的宽度的变化会影响其子元素的宽度,其子元素宽度的变化也会继续对其孙子元素产生影响。因此对于浏览器来说,布局过程是经常发生的。
  • Paint:绘制,本质上就是填充像素的过程。包括绘制文字、颜色、图像、边框和阴影等,也就是一个 DOM 元素所有的可视效果。一般来说,这个绘制过程是在多个层上完成的。
  • Composite:渲染层合并,由上一步可知,对页面中 DOM 元素的绘制是在多个层上进行的。在每个层上完成绘制过程之后,浏览器会将所有层按照合理的顺序合并成一个图层,然后显示在屏幕上。对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。

当然,本文我们只来关注 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 到 LayoutObjects

DOM 树中得每个 Node 节点都有一个对应的 LayoutObject 。LayoutObject 知道如何在屏幕上 paint Node 的内容。

从 LayoutObjects 到 PaintLayers

一般来说,拥有相同的坐标空间的 LayoutObjects,属于同一个渲染层(PaintLayer)。PaintLayer 最初是用来实现 stacking contest(层叠上下文),以此来保证页面元素以正确的顺序合成(composite),这样才能正确的展示元素的重叠以及半透明元素等等。因此满足形成层叠上下文条件的 LayoutObject 一定会为其创建新的渲染层,当然还有其他的一些特殊情况,为一些特殊的 LayoutObjects 创建一个新的渲染层,比如 overflow != visible 的元素。根据创建 PaintLayer 的原因不同,可以将其分为常见的 3 类:

  • NormalPaintLayer
    • 根元素(HTML)
    • 有明确的定位属性(relative、fixed、sticky、absolute)
    • 透明的(opacity 小于 1)
    • 有 CSS 滤镜(fliter)
    • 有 CSS mask 属性
    • 有 CSS mix-blend-mode 属性(不为 normal)
    • 有 CSS transform 属性(不为 none)
    • backface-visibility 属性为 hidden
    • 有 CSS reflection 属性
    • 有 CSS column-count 属性(不为 auto)或者 有 CSS column-width 属性(不为 auto)
    • 当前有对于 opacity、transform、fliter、backdrop-filter 应用动画
  • OverflowClipPaintLayer
    • overflow 不为 visible
  • NoPaintLayer
    • 不需要 paint 的 PaintLayer,比如一个没有视觉属性(背景、颜色、阴影等)的空 div。

满足以上条件的 LayoutObject 会拥有独立的渲染层,而其他的 LayoutObject 则和其第一个拥有渲染层的父元素共用一个。

从 PaintLayers 到 GraphicsLayers

某些特殊的渲染层会被认为是合成层(Compositing Layers),合成层拥有单独的 GraphicsLayer,而其他不是合成层的渲染层,则和其第一个拥有 GraphicsLayer 父层公用一个。

每个 GraphicsLayer 都有一个 GraphicsContext,GraphicsContext 会负责输出该层的位图,位图是存储在共享内存中,作为纹理上传到 GPU 中,最后由 GPU 将多个位图进行合成,然后 draw 到屏幕上,此时,我们的页面也就展现到了屏幕上。

渲染层提升为合成层的原因有一下几种:

注:渲染层提升为合成层有一个先决条件,该渲染层必须是 SelfPaintingLayer(基本可认为是上文介绍的 NormalPaintLayer)。以下所讨论的渲染层提升为合成层的情况都是在该渲染层为 SelfPaintingLayer 前提下的。

  • 直接原因(direct reason)
    • 硬件加速的 iframe 元素(比如 iframe 嵌入的页面中有合成层)demo
    • video 元素
    • 覆盖在 video 元素上的视频控制栏
    • 3D 或者 硬件加速的 2D Canvas 元素
    • 硬件加速的插件,比如 flash 等等
    • 在 DPI 较高的屏幕上,fix 定位的元素会自动地被提升到合成层中。但在 DPI 较低的设备上却并非如此,因为这个渲染层的提升会使得字体渲染方式由子像素变为灰阶(详细内容请参考:Text Rendering
    • 有 3D transform
    • backface-visibility 为 hidden
    • 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition(需要是 active 的 animation 或者 transition,当 animation 或者 transition 效果未开始或结束后,提升合成层也会失效)
    • will-change 设置为 opacity、transform、top、left、bottom、right(其中 top、left 等需要设置明确的定位属性,如 relative 等)demo
  • 后代元素原因
    • 有合成层后代同时本身有 transform、opactiy(小于 1)、mask、fliter、reflection 属性 demo
    • 有合成层后代同时本身 overflow 不为 visible(如果本身是因为明确的定位因素产生的 SelfPaintingLayer,则需要 z-index 不为 auto) demo
    • 有合成层后代同时本身 fixed 定位 demo
    • 有 3D transfrom 的合成层后代同时本身有 preserves-3d 属性 demo
    • 有 3D transfrom 的合成层后代同时本身有 perspective 属性 demo
  • overlap 重叠原因为什么会因为重叠原因而产生合成层呢?举个简单的栗子。蓝色的矩形重叠在绿色矩形之上,同时它们的父元素是一个 GraphicsLayer。此时假设绿色矩形为一个 GraphicsLayer,如果 overlap 无法提升合成层的话,那么蓝色矩形不会提升为合成层,也就会和父元素公用一个 GraphicsLayer。此时,渲染顺序就会发生错误,因此为保证渲染顺序,overlap 也成为了合成层产生的原因,也就是如下的正常情形。当然 overlap 的原因也会细分为几类,接下来我们会详细看下。
    • 重叠或者说部分重叠在一个合成层之上。那如何算是重叠呢,最常见和容易理解的就是元素的 border box(content + padding + border) 和合成层的有重叠,比如:demo,当然 margin area 的重叠是无效的(demo)。其他的还有一些不常见的情况,也算是同合成层重叠的条件,如下:
      • filter 效果同合成层重叠 demo
      • transform 变换后同合成层重叠 demo
      • overflow scroll 情况下同合成层重叠。即如果一个 overflow scroll(不管 overflow:auto 还是 overflow:scrill,只要是能 scroll 即可) 的元素同一个合成层重叠,则其可视子元素也同该合成层重叠 demo
    • 假设重叠在一个合成层之上(assumedOverlap)。这个原因听上去有点虚,什么叫假设重叠?其实也比较好理解,比如一个元素的 CSS 动画效果,动画运行期间,元素是有可能和其他元素有重叠的。针对于这种情况,于是就有了 assumedOverlap 的合成层产生原因,示例可见:demo。在本 demo 中,动画元素视觉上并没有和其兄弟元素重叠,但因为 assumedOverlap 的原因,其兄弟元素依然提升为了合成层。需要注意的是该原因下,有一个很特殊的情况:如果合成层有内联的 transform 属性,会导致其兄弟渲染层 assume overlap,从而提升为合成层。比如:demo

层压缩

基本上常见的一些合成层的提升原因如上所说,你会发现,由于重叠的原因,可能随随便便就会产生出大量合成层来,而每个合成层都要消耗 CPU 和内存资源,岂不是严重影响页面性能。这一点浏览器也考虑到了,因此就有了层压缩(Layer Squashing)的处理。如果多个渲染层同一个合成层重叠时,这些渲染层会被压缩到一个 GraphicsLayer 中,以防止由于重叠原因导致可能出现的“层爆炸”。具体可以看如下 demo。一开始,蓝色方块由于
translateZ 提升为了合成层,其他的方块元素因为重叠的原因,被压缩了一起,大小就是包含这 3 个方块的矩形大小。

当我们 hover 绿色方块时,会给其设置 translateZ 属性,导致绿色方块也被提升为合成层,则剩下的两个被压缩到了一起,大小就缩小为包含这 2 个方块的矩形大小。

当然,浏览器的自动的层压缩也不是万能的,有很多特定情况下,浏览器是无法进行层压缩的,如下所示,而这些情况也是我们应该尽量避免的。(注:以下情况都是基于重叠原因而言)

  • 无法进行会打破渲染顺序的压缩(squashingWouldBreakPaintOrder)示例如下:demo

  • video 元素的渲染层无法被压缩同时也无法将别的渲染层压缩到 video 所在的合成层上(squashingVideoIsDisallowed)demo
  • iframe、plugin 的渲染层无法被压缩同时也无法将别的渲染层压缩到其所在的合成层上(squashingLayoutPartIsDisallowed)demo
  • 无法压缩有 reflection 属性的渲染层(squashingReflectionDisallowed)demo
  • 无法压缩有 blend mode 属性的渲染层(squashingBlendingDisallowed)demo
  • 当渲染层同合成层有不同的裁剪容器(clipping container)时,该渲染层无法压缩(squashingClippingContainerMismatch)。示例如下:demo