1
头图
在日常的开发中,通过使用css属性,做一些动效、动画时,会发现在页面有卡顿;
在Android低端机尤为明显,故需要知道浏览器渲染以及优化手段

浏览器渲染流程

image.png
image.png

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

以上是浏览器渲染的基本步骤,简述:

DOM tree + CSS tree  == Render tree  ==>   Layout tree  ==>  PaintLayer => Composite
                                           PaintLayer => Composite
                                            

只要页面上元素有改变,都会重复渲染以上部分步骤,在一帧内完成(16.77ms);
通过设置css 样式 ,做动画,在一帧内无法渲染完,甚至来不及渲染,就会卡顿;
其中部分渲染卡顿,跟渲染层合成(Composite)有关系。

Composite

什么是渲染层合成

在 DOM 树中每个节点都会对应一个渲染对象(RenderObject),当它们的渲染对象处于相同的坐标空间(z 轴空间)时,就会形成一个 RenderLayers,也就是渲染层。渲染层将保证页面元素以正确的顺序堆叠,这时候就会出现层合成(composite),从而正确处理透明元素和重叠元素的显示,这步叫做“层叠上下文”
这个模型类似于 Photoshop 的图层模型,在 Photoshop 中,每个设计元素都是一个独立的图层,多个图层以恰当的顺序在 z 轴空间上叠加,最终构成一个完整的设计图。
对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。

image.png

渲染对象(RenderObject)

一个 DOM 节点对应了一个渲染对象,渲染对象依然维持着 DOM 树的树形结构。

渲染层(RenderLayer)

这是浏览器渲染期间构建的第一个层模型,处于相同坐标空间(z轴空间)的渲染对象,都将归并到同一个渲染层中,因此根据层叠上下文,不同坐标空间的的渲染对象将形成多个渲染层,以体现它们的层叠关系。
因此满足形成层叠上下文条件的 RenderObject 一定会为其创建新的渲染层,当然还有其他的一些特殊情况,为一些特殊的 RenderObject 创建一个新的渲染层,比如 overflow != visible 的元素。根据创建 RenderLayer 的原因不同,可以将其分为常见的 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。

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

图形层(GraphicsLayer)

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

合成层(CompositingLayer)

满足某些特殊条件的渲染层,会被浏览器自动提升为合成层。合成层拥有单独的 GraphicsLayer,
个渲染层满足哪些特殊条件时,才能被提升为合成层呢?这里列举了一些常见的情况:

  • 3D transforms:translate3d、translateZ 等
  • video、canvas、iframe 等元素
  • 通过 Element.animate() 实现的 opacity 动画转换
  • 通过 СSS 动画实现的 opacity 动画转换
  • position: fixed
  • 具有 will-change 属性
  • 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition

常见实用**transforms:translate3d、translateZ ** 来形成合成层,这样是对原先的布局没有影响

示例

image.png
image.png

其他合成情况

隐式合成

除此之外,在浏览器的 Composite 阶段,还存在一种隐式合成,部分渲染层在一些特定场景下,会被默认提升为合成层。其实就是元素div之间相互重叠
image.png
上图所示:把position-1生成合成层,后面的元素会相互重叠,为了层叠上下文展示正确;导致后面的元素直接隐式合成了;

合层压缩

如上图,目前没有遇到

合成爆炸

通过上图研究,很容易就产生一些不在预期范围内的合成层,当这些不符合预期的合成层达到一定量级时,就会变成层爆炸。
需要排查是否有重叠,能否通过z-index来调节层叠顺序

优化

优点

  • 合成层的位图,会交由 GPU 合成,比 CPU 处理要快得多;
  • 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层;
  • 元素提升为合成层后,transform 和 opacity 才不会触发 repaint,如果不是合成层,则其依然会触发 repaint。(只有css动画,才可以,js去控制还是会重排、重绘的)使用 transform 或者 opacity 来实现动画效果

更新元素几何属性(发生重排)
image.png
浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的
引发重排的情况

  • 页面首次渲染;
  • 浏览器窗口大小发生变化;
  • 元素的内容发生变化;
  • 元素的尺寸或者位置发生变化;
  • 元素的字体大小发生变化;
  • 激活CSS伪类;
  • 查询某些属性或者调用某些方法;

    • offsetTop、offsetLeft、offsetWidth、offsetHeight
    • scrollTop、scrollLeft、scrollWidth、scrollHeight
    • clientTop、clientLeft、clientWidth、clientHeight
    • width、height
    • getComputedStyle()
    • getBoundingClientRect()
    • 添加或者删除可见的DOM元素。

更新元素绘制属性(发生重绘)
image.png
如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些
引发重绘的情况

  • 重排
  • visibility: visible <=> hidden
  • 颜色改变
  • 其他几何变化...

直接合成阶段
image.png
我们使用了CSS的transform来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。
缺点

  • 绘制的图层必须传输到 GPU,这些层的数量和大小达到一定量级后,可能会导致传输非常慢,进而导致一些低端和中端设备上出现闪烁;
  • 隐式合成容易产生过量的合成层,每个合成层都占用额外的内存,而内存是移动设备上的宝贵资源,过多使用内存可能会导致浏览器崩溃,让性能优化适得其s

    其他优化

    css

  • 使用 transform 替代 top
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局
  • 避免使用table布局,可能很小的一个小改动会造成整个 table 的重新布局。
  • 尽可能在DOM树的最末端改变class,回流是不可避免的,但可以减少其影响。尽可能在DOM树的最末端改变class,可以限制了回流的范围,使其影响尽可能少的节点。
  • 避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。
  • 将动画效果应用到position属性为absolute或fixed的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择 requestAnimationFrame,详见探讨 requestAnimationFrame
  • 避免使用CSS表达式,可能会引发回流。
  • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如will-change、video、iframe等标签,浏览器会自动将该节点变为图层。

    js:

  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

    总结

    在做动画时尽可能的使用使用csstransforms属性做动画,,它跳过Layout和Paint阶段,直接进入渲染,所以会提升部分性能,同时看看所属空间位置,是否提升了合成层;还有注意元素是否重叠,防止合成层爆炸;这样至少能保证css 方面,尽可能性能没有问题。
    剩下的请查看其他优化手段

示例代码

重叠

<style>
 .ball-running,.ball-running1 {
      width: 100px;
      height: 100px;
      animation: run-around 4s linear alternate 100;
      background: red;
      position: absolute;
      border-radius: 50%;
    }
    @keyframes run-around {
      0% {
        top: 0;
        left: 0;
      }
      25% {
        top: 0;
        left: 200px;
      }
      50% {
        top: 200px;
        left: 200px;
      }
      75% {
        top: 200px;
        left: 0;
      }
    }
    .position-1,.position-2,.position-3,.position-4 {
      position: absolute;
      width: 100px;
      height: 100px;
      background-color: red;
      border: #1b1b1b 1px solid;
    }
    .position-1 {
      transform: translateZ(10px);
    }
    .position-2 {
      top: 90px;
      left: 90px;
     // transform: translateZ(10px);
    }
    .position-3 {
      top: 180px;
      left: 180px;
    }
    .position-4 {
      top: 270px;
      left: 270px;
    }
    .parent-1 {
      //width: 100px;
      //height: 200px;
      background: #5b3a13;
      //overflow: auto;
      //transform: translateZ(10px);
    }
    .position-5 {

    }
<style/>    
<div class="parent-1">
      <div class="position-1">
        1
      </div>
      <div class="position-2">
        2
      </div>
      <div class="position-3">
        3
      </div>
      <div class="position-4">
        4
      </div>
    </div>

动画

<style>
.ball-running,.ball-running1 {
      width: 100px;
      height: 100px;
    //  animation: run-around2 4s linear alternate 100;
      background: red;
      position: absolute;
      border-radius: 50%;
    }
    @keyframes run-around {
      0% {
        top: 0;
        left: 0;
      }
      25% {
        top: 0;
        left: 200px;
      }
      50% {
        top: 200px;
        left: 200px;
      }
      75% {
        top: 200px;
        left: 0;
      }
    }





    //.ball-running {
    //  width: 100px;
    //  height: 100px;
    //  animation: run-around 2s linear alternate 100;
    //  background: red;
    //  border-radius: 50%;
    //}
    //@keyframes run-around {
    //  0% {
    //    transform: translate(0, 0);
    //  }
    //  25% {
    //    transform: translate(200px, 0);
    //  }
    //  50% {
    //    transform: translate(200px, 200px);
    //  }
    //  75% {
    //    transform: translate(0, 200px);
    //  }
    //}

</style>
<div class="ball-running"></div>

参考:

transform和left改变位置的性能区别
GPU Accelerated Compositing in Chrome
浏览器层合成与页面渲染优化
浏览器渲染页面过程与页面优化
无线性能优化:Composite
浏览器渲染过程
渲染流程
Stick to Compositor-Only Properties and Manage Layer Count


散一群逗逼
554 声望508 粉丝

做一位有逼格的前端工程师