性能优化——重绘重排

带你重新认识浏览器的渲染机制

Posted by Shiyanping on February 12, 2019

性能优化中,减少重绘重排应该是一种很好的优化方式,我们具体看一下什么情况下会造成重绘重排,为什么减少重绘重排可以做到优化,怎么样减少重绘重排。

浏览器渲染过程

我们先看看当浏览器拿到服务端返回的资源时,是如何渲染的。

首先浏览器会进行文件解析,主要解析三个东西:

  1. 解析 html/xhtml/svg,形成 dom 树。
  2. 解析 css,产生 CSS Rule Tree。
  3. 解析 js,js 会通过 api(包括 DOM API 和 CSSOM API) 来操作 Dom Tree 和 CSS Rule Tree。

解析完成之后

  1. 通过 CSS Rule Tree 和 Dom Tree 生成 rendering Tree。其中不包括 display:none 的元素。
  2. 计算 DOM 节点的位置,对元素进行分层及布局,也叫 reflow 和 layout 过程。

布局完成之后,就要进行绘制了,将各层发给 GPU,GPU 将各层合成,显示在屏幕上,即 composite。

什么情况下会造成重绘重排

当我们开始绘制的时候,如果使用 js 操作了 dom 元素,或者改变了 css 属性,就可能会造成重绘(repaint)和重排(reflow)。

repaint:屏幕的一部分进行了重画,比如某个 css 中改变背景色,元素尺寸没有变。 reflow:任何一个元素的尺寸发生了变化,需要重新验证并计算 render tree,就会造成重排。

在 PC 时代,我们用 jquery 进行获取元素,改变元素的尺寸,及时发生重排,我们也很难感知到,但是当移动时代到来之后,如果频繁发生重排,那手机就会受不了了。

尤其是在执行下面操作时,成本会很高:

  1. js 添加或者删除元素的时候
  2. js 改变元素的位置发生改变时
  3. 页面初始化
  4. 容器尺寸发生变化。比如在标准盒模型下添加 padding,border 就会造成重排,所以在书写样式的时候,很多会将盒模型设计成怪异盒模型(box-sizing: border-box)
  5. js 获取元素的尺寸单位。
    • offsetTop, offsetLeft, offsetWidth, offsetHeight
    • scrollTop/Left/Width/Height
    • clientTop/Left/Width/Height
    • IE 中的 getComputedStyle(), 或 currentStyle

如果发生上述的行为基本都会造成重绘和重排。

当发生重排时,一定会发生重绘,但是发生重绘不一定会发生重排。

浏览器中每个元素节点都有 reflow 方法,当一个元素发生 reflow 时,他的子节点都会发生 reflow。

举几个例子来说明一下造成重绘重排的情况:

1
2
3
4
5
6
7
8
9
10
11
12
var bodyStyle = document.body.style; // cache

bodyStyle.padding = '20px'; // reflow, repaint
bodyStyle.border = '10px solid red'; //  再一次的 reflow 和 repaint

bodyStyle.color = 'blue'; // repaint
bodyStyle.backgroundColor = '#fad'; // repaint

bodyStyle.fontSize = '2em'; // reflow, repaint

// new DOM element - reflow, repaint
document.body.appendChild(document.createTextNode('children!'));

为什么减少重绘重排可以优化性能

前面说了浏览器的渲染机制,多一次重绘就需要浏览器重新进行一次绘制,及时 GPU 处理会比较快,但是也是吃不消的,更别说重排了,重排一个 dom,会重新生成 render Tree,然后重新绘制。

如何减少重绘重排

其实浏览器很聪明,不可能每次修改样式就 reflow 或者 repaint 一次,一般来说,浏览器会积累一批操作,然后做一次 reflow。

但是也有些例外情况,比如 resize 窗口,改变窗口字体,浏览器会立即进行 reflow。

虽然浏览器会这么做,但是我们也应该减少重绘重排的次数,在开发阶段就为浏览器进行特殊的关爱,毕竟是每天陪伴我们的小伙伴。

下面总结了一些针对 reflow 和 repaint 的最佳实践:

  1. 不要一条一条的修改 DOM 样式,尽量提前设置好 class,后续增加 class,进行批量修改。
  2. 把 DOM 离线后修改。
    • 使用 documentFragment 对象在内存里操作 DOM。
    • 使用 requestAnimationFrame 可以进行优化,在下一帧进行操作。
    • 把修改频繁的元素先 display: none,修改完之后显示,修改个 100 次也无妨。
    • clone 一个 dom 节点在内存里,修改之后;与在线的节点相替换。
  3. 不要把 DOM 元素的属性值放到一个循环中当成循环的变量。
  4. 尽可能的修改低层级的节点,避免修改高层级的节点,造成大面积的 reflow。
  5. 千万不要使用 table 布局,修改一小块地方,会造成整个 table 重新布局。
  6. 动画尽量使用 css 动画,css 动画中尽量只使用 transform 和 opacity,这不会发生重排和重绘

composite

当每次布局完成之后,就会发生 composite 过程,浏览器都把重绘后的图像发给 GPU 去合成并显示。

在上面最佳实践中最后提到了动画,动画其实是比较耗费性能的,因为动画的每一帧都会发给 GPU 去合成,重绘重排会发生在动画的每一帧。

我们在写动画的时候,可以通过 js 写,也可以通过 css 写。两种方式在写动画时,过程也是不一样的。

  1. js 写的动画,过程:js 计算 -> 重排(若布局改变) -> 重绘 -> 合成
  2. css 动画,过程:重排(若布局改变)-> 重绘 -> 合成

所以不难看出,耗费性能最少并能并最流畅的动画是只触发合成。

为了仅发生 composite,我们做动画的 css property 必须满足以下三个条件:

  1. 不影响文档流。
  2. 不依赖文档流。
  3. 不会造成重绘。

满足以上以上条件的 css property 只有 transform 和 opacity。

这样的话,由于没有重排和重绘,只有合成,那么浏览器在动画执行之前就知道动画如何开始和结束。

并且有两个优势:

  1. 动画将会非常流畅
  2. 动画不在绑定到 CPU,即使 js 执行大量的工作;动画依然流畅

事实上影响动画流畅性的因素不止重排重绘,还有 CPU 内存。

css 动画有一个重要的特性,它是完全工作在 GPU 上。因为你声明了一个动画如何开始和如何结束,浏览器会在动画开始前准备好所有需要的指令;并把它们发送给 GPU。

而如果使用 js 动画,浏览器必须计算每一帧的状态;为了保证平滑的动画,我们必须在浏览器主线程计算新状态;把它们发送给 GPU 至少 60 次每秒。

除了计算和发送数据比 css 动画要慢,主线程的负载也会影响动画; 当主线程的计算任务过多时,会造成动画的延迟、卡顿。

所以最佳实践中最后一条就提到了,在写动画时,尽量写 css 动画,并且尽量用 transform 和 opacity。

辅助工具

谷歌浏览器检测重绘工具:右上角…,more tools,rendering。

勾选了 paint flashing 时,在刷新页面之后,就会发现有有绿色的背景,有绿色的背景就代表着重排。

还可以勾选 FPS meter,可以查看渲染页面时候 GPU 的使用率。

方便发现自己在开发过程中,哪些操作造成了重绘重排,尽量减少修改次数,优化性能。