浏览器渲染页面流程

image.png
大致可以分为 构建DOM树 -> 构建渲染树 -> 布局 -> 绘制 -> 渲染层合成几个步骤。

  • 构建DOM树:浏览器将HTML解析成DOM树,发生在页面初次加载或通过JavaScript修改DOM元素时。
  • 构建渲染树:浏览器将CSS解析成CSSOM树,然后和DOM树合并生成渲染树(renderTree)。
  • 布局(Layout):浏览器根据渲染树所体现的节点、各个节点的CSS定义以及它们的从属关系,计算出每个节点在屏幕中的位置。Web 页面中元素的布局是相对的,在页面元素位置、大小发生变化,往往会导致其他节点联动,需要重新计算布局,这时候的布局过程一般被称为回流(Reflow)。
  • 绘制(Paint):遍历渲染树,调用渲染器的 paint() 方法在屏幕上绘制出节点内容,本质上是一个像素填充的过程。这个过程也出现于回流或一些不影响布局的 CSS 修改引起的屏幕局部重绘(Repaint)。一般来说,绘制过程是在多个层上完成的,这些层我们称为渲染层(RenderLayer)。
  • 渲染层合成(Composite):上一步可知绘制是在多个层上进行的,在每个层上完成绘制过程之后,浏览器会将所有层按照合理的顺序合并成一个图层,然后显示在屏幕上。对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。

浏览器渲染原理

从浏览器的渲染过程中我们知道,页面 HTML 会被解析成 DOM 树,每个 HTML 元素对应了树结构上的一个 node 节点。而从 DOM 树转化到一个个的渲染层,并最终执行合并、绘制的过程,中间其实还存在一些过渡的数据结构,它们记录了 DOM 树到屏幕图形的转化原理,其本质也就是树结构到层结构的演化。

image.png
在Chrome中其实有几种不同的层类型:

  • PaintLayers 渲染层,负责对应DOM子树
  • GraphicsLayers 图形层,负责对应PaintLayers子树

Chrome 是如何将 DOM 转变成一个屏幕图像的呢?从概念上讲,它:

  1. 获取 DOM 并将其分割为多个层
  2. 将每个层独立的绘制进位图中
  3. 将层作为纹理上传至 GPU
  4. 复合多个层来生成最终的屏幕图像。

渲染对象(LayoutObject)

DOM树中每一个Node节点对应一个LayoutObject, LayoutObject知道如何在屏幕上绘制一个节点的内容,它通过向一个绘图上下文(GraphicsContext)发出必要的绘制调用来绘制节点。

渲染层(PaintLayers)

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

  • NormalPaintLayer
    • 根元素(HTML)
    • 有明确的定位属性(relative、fixed、sticky、absolute)
    • 透明的(opacity < 1)
    • 有css滤镜(filter)
    • 有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、filter、backdrop-filter 应用动画
  • OverflowClipPaintLayer
    • overflow不为visible
  • NoPaintLayer
    • 不需要paint的paintLayer,比如一个没有视觉属性(背景、颜色、阴影等)的空div

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

图形层(GraphicsLayers)

每个GraphicsLayers都有一个图形上下文(GraphicsContext), GraphicsContext负责输出该层的位图,位图存储在共享内存中,作为纹理上传到GPU,最后由GPU将多个位图进行合成,然后绘制到屏幕上,此时我们的页面就展示到了屏幕上。
所以GraphicsLayer是一个重要的渲染载体和工具,但它并不直接处理渲染层,而是处理合成层。

什么是纹理?可以把它想象成一个从主存储器(例如 RAM)移动到图像存储器(例如 GPU 中的 VRAM)的位图图像(bitmap image)

合成层(CompositingLayers)

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

渲染层提升为合成层的条件:

  • 3D transforms: translate3d、translateZ等
  • video元素
  • 覆盖在video元素上的视频控制栏
  • 3D 或者 硬件加速的 2D Canvas 元素
  • 硬件加速的 iframe 元素(比如 iframe 嵌入的页面中有合成层)
  • 用过ELement.animate() 实现的opacity动画转换
  • 通过css动画实现的opacity动画转换
  • position:fixed
  • 具有will-change属性
  • 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition(需要是 active 的 animation 或者 transition,当 animation 或者 transition 效果未开始或结束后,提升合成层也会失效)

以上列举是部分常见的例子,无线性能优化:Composite这篇文章描述的很详细,这里就不赘述了

隐式合成

上边提到,满足某些显性的特殊条件时,渲染层会被浏览器提升为合成层。除此之外,在浏览器的 Composite 阶段,还存在一种隐式合成,部分渲染层在一些特定场景下,会被默认提升为合成层。
对于隐式合成,CSS GPU Animation 中是这么描述的:

This is called implicit compositing: One or more non-composited elements that should appear above a composited one in the stacking order are promoted to composite layers. (一个或多个非合成元素应出现在堆叠顺序上的合成元素之上,被提升到合成层。)

例如:
两个 absolute 定位的 div 在屏幕上交叠了,根据 z-index 的关系,其中一个 div 就会”盖在“了另外一个上边。
image.png

此时,如果z-index:3的div被加上了 transform: translateZ(0) 就会被浏览器提升为合成层。提升后的合成层位于Document上方,假如没有隐式合成,原本应该处于上方的 div 就依然还是跟 Document 共用一个 GraphicsLayer,层级反而降了,就出现了元素交叠关系错乱的问题。
image.png
所以为了纠正错误的交叠顺序,浏览器必须让原本应该“盖在”它上面的渲染层也同时提升为合成层。
image.png

层爆炸

从上边的研究中我们可以发现,一些产生合成层的原因太过于隐蔽了,尤其是隐式合成。在平时的开发过程中,我们很少会去关注层合成的问题,很容易就产生一些不在预期范围内的合成层,当这些不符合预期的合成层达到一定量级时,就会变成层爆炸。
层爆炸会占用 GPU 和大量的内存资源,严重损耗页面性能,因此盲目地使用 GPU 加速,结果有可能会是适得其反。

层压缩

如果多个渲染层同一个合成层重叠时,这些渲染层会被压缩到一个 GraphicsLayer 中,以防止由于重叠原因导致可能出现的“层爆炸”。

image.png
浏览器的层压缩机制,会将隐式合成的多个渲染层压缩到同一个GraphicsLayer中进行渲染。所以在z-index:3被加上transform: translateZ(0) 提升为合成层之后,其上方的三个div最终会处于同一个合成层中。

当然,浏览器的自动层压缩并不是万能的,有很多特定情况下,浏览器是无法进行层压缩的。
无线性能优化:Composite 这篇文章列举了许多详细的场景。

合成层作用(优缺点)

  • 优点
    • 合成层的位图,会交给GPU处理,比CPU处理要快得多
    • 当需要重绘时,只需要重绘本身,不会影响到其他的层
    • 元素提升为合成层后,transform 和 opacity 才不会触发 repaint,如果不是合成层,则其依然会触发 repaint。
  • 缺点
    • 绘制的图层必须传输到 GPU,这些层的数量和大小达到一定量级后,可能会导致传输非常慢,进而导致一些低端和中端设备上出现闪烁;
    • 隐式合成容易产生过量的合成层,每个合成层都占用额外的内存,而内存是移动设备上的宝贵资源,过多使用内存可能会导致浏览器崩溃,让性能优化适得其反。

性能优化

提升动画效果的元素

合成层的好处是不会影响到其他元素的绘制,因此,为了减少动画元素对其他元素的影响,从而减少 paint,我们需要把动画效果中的元素提升为合成层。
提升合成层的最好方式是使用 CSS 的 will-change 属性。从合成层产生条件中,可以知道 will-change 设置为 opacity、transform、top、left、bottom、right 可以将元素提升为合成层。

  1. #target {
  2. will-change: transform;
  3. }

其兼容性如下所示:
image.png
对于那些目前还不支持 will-change 属性的浏览器,目前常用的是使用一个 3D transform 属性来强制提升为合成层:

  1. #target {
  2. transform: translateZ(0);
  3. }

但需要注意的是,不要创建太多的渲染层。因为每创建一个新的渲染层,就意味着新的内存分配和更复杂的层的管理。之后我们会详细讨论。
如果你已经把一个元素放到一个新的合成层里,那么可以使用 Timeline 来确认这么做是否真的改进了渲染性能。别盲目提升合成层,一定要分析其实际性能表现。

使用transform或者opacity来实现动画效果

注意:元素提升为合成层后,transform 和 opacity 才不会触发 paint,如果不是合成层,则其依然会触发 paint。

减小绘制区域

对于不需要重新绘制的区域应尽量避免绘制,以减少绘制区域,比如一个 fix 在页面顶部的固定不变的导航 header,在页面内容某个区域 repaint 时,整个屏幕包括 fix 的 header 也会被重绘。
而对于固定不变的区域,我们期望其并不会被重绘,因此可以通过之前的方法,将其提升为独立的合成层。
减少绘制区域,需要仔细分析页面,区分绘制区域,减少重绘区域甚至避免重绘。

合理管理合成层

创建一个新的合成层并不是免费的,它得消耗额外的内存和管理资源。实际上,在内存资源有限的设备上,合成层带来的性能改善,可能远远赶不上过多合成层开销给页面性能带来的负面影响。同时,由于每个渲染层的纹理都需要上传到 GPU 处理,因此我们还需要考虑 CPU 和 GPU 之间的带宽问题、以及有多大内存供 GPU 处理这些纹理的问题。

参考:
详谈层合成
无线性能优化:Composite
[

](https://fed.taobao.org/blog/taofed/do71ct/performance-composite/?spm=taofed.blogs.header.7.63e65ac801qdAI)