重绘和回流

概述

当 render tree 中的一部分(或全部)因为元素的尺寸、布局、显示/隐藏等改变而需要重新构建,这个过程称作回流(reflow)。页面第一次加载的时候,至少发生一次回流

当 render tree 中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如 background-color,这个过程叫做重绘(repaint)

在回流的时候,浏览器会使 render tree 中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。因此回流必将引起重绘,而重绘不一定会引起回流。

Reflow 的成本比 Repaint 高得多的多。DOM Tree 里的每个结点都会有 reflow 方法,一个结点的 reflow 很有可能导致子结点,甚至父点以及同级结点的 reflow。

重绘的主体是节点

回流的主体是DOM 树,既可以是子树也可以是根树,根树的回流又称为页面回流

为什么说 DOM 操作很慢

就是因为会频繁的触发回流和重绘。DOM 本身只是 JS 对象,修改 DOM 树的部分是很快的

在 DOM 查询时,querySelectorquerySelectorAll 应该是最后的选择,它们功能最强大,但执行效率很差,如果可以的话,尽量用其他方法替代。

在 Chrome 中查看 Repaint

F12 打开控制台 -> DevTools -> more tools -> Rendering -> 勾选 Paint flashing,layer borders

查看 tiemline,通过 performance 录制查看

重绘何时发生

当一个元素的外观的可见性 visibility 发生改变的时候,但是不影响布局。 比如:

回流何时发生

Document

页面渲染初始化

调整窗口大小

整个页面回流

Css

影响至少一级的父级元素,默认继承的子元素

并没有直接导致回流,是否回流取决于改变的属性

如果是独占一行的块级元素,改变宽度不会发生回流

绝对定位的元素,改变 top\left\right\bottom 不会回流,仅重绘

类似的 transform 也是这样, 仅重绘

影响到父级

Dom

脚本操作 DOM,增加删除或者修改 DOM 节点,元素尺寸改变——边距、填充、边框、宽度和高度。

一些常用且会导致回流的属性和方法:

  var s = document.body.style
  s.padding = "2px" // 回流+重绘
  s.border = "1px solid red" // 回流+重绘
  s.color = "blue" // 重绘
  s.backgroundColor = "#ccc" // 重绘
  s.fontSize = "14px" // 再一次 回流+重绘
  document.body.appendChild(document.createTextNode('abc!')) // 回流+重绘

浏览器的队列优化

如果向上述代码中那样,浏览器不停地回流 + 重绘,很可能性能开销非常大,实际上浏览器会优化这些操作,将所有引起回流和重绘的操作放入一个队列中,等待队列达到一定的数量或者时间间隔,就 flush 这个队列,一次性处理所有的回流和重绘。

虽然有浏览器优化,但是当我们向浏览器请求一些 style 信息的时候,浏览器为了确保我们能拿到精确的值,就会提前 flush 队列。

包括:

减少回流重绘

Css

为 HTML 标签使用 fixed 或 aboslute 的 position,那么修改他们的 CSS 是不会 reflow 的

translateXY 会在自己的图层内重绘,而 left top 会导致整个页面重绘,性能差距,在移动端一般使用 translateXY 实现轮播效果(无缝划屏)

requestAnimationFrame:能保证浏览器在正确的时间进行渲染。

Js

保持 DOM 操作“原子性”:

  // bad
  var newWidth = ele.offsetWidth + 10
  ele.style.width = newWidth + 'px'
  
  var newHeight = ele.offsetHeight + 10
  ele.style.height = newHeight + 'px'
  
  // good 读写分离,批量操作
  var newWidth = ele.offsetWidth + 10 // read
  var newHeight = ele.offsetHeight + 10 // read
  ele.style.width = newWidth + 'px' // write
  ele.style.height = newHeight + 'px' // write

使用 classList 代替 className:

className 只要赋值,就一定出现一次 rendering 计算;classList 的 add 和 remove,浏览器会进行样式名是否存在的判断,以减少重复的 rendering。

  ele.className += 'something'
  ele.classList.add('something')
  ele.classList.remove('something')

批量操作借助临时变量

// bad
for (let i = 0; i < 10; i++) {
  el.style.left = el.offsetLeft + 5 + 'px'
  el.style.top = el.offsetTop + 5 + 'px' 
}
// good
let left = el.offsetLeft
let top = el.offsetTop
for (let i = 0; i < 10; i++) {
  left += 5
  top += 5 
}
el.style.left = left + 'px'
el.style.top = left + 'px'

JS 离线操作

对元素进行“离线操作”,完成后再一起更新:

示例

假如需要在下面的 html 中添加两个 li 节点:

<ul id="">
</ul>
  let ul = document.getElementByTagName('ul')
  let man = document.createElement('li')
  man.innerHTML = 'man'
  ul.appendChild(li)
   
  let woman = document.createElement('li')
  woman.innerHTML = 'woman'
  ul.appendChild(woman)

上述代码会发生两次回流,假如使用 display: none 的方案,虽然能够减少回流次数,但是会发生一次闪烁,这时候使用 DocumentFragment 的优势就体现出来了。

DocumentFragment 有两大特点:

  let fragment = document.createDocumentFragment()
  
  let man = document.createElement('li')
  let woman = document.createElement('li')
  man.innerHTML = 'man'
  woman.innerHTML = 'woman'
  fragment.appendChild(man)
  fragment.appendChild(woman)
  
  document.body.appendChild(spanNode)

可见 DocumentFragment 是一个孤儿节点,没爹就能出生,但是在需要它的时候,它又无私地把孩子奉献给文档树,然后自己默默离开。是不是有点像《银翼杀手 2049》?

CSS 硬件加速

参考:https://juejin.im/post/5b6143996fb9a04fd343ae28#heading-4

CSS3 硬件加速又叫做 GPU 加速,是利用 GPU 进行渲染,减少 CPU 操作的一种优化方案。由于 GPU 中的 transform 等 CSS 属性不会触发 repaint,所以能大大提高网页的性能。

动画与帧

之前学习 flash 的时候,就知道动画是由一帧一帧的图片组成,在浏览器中也是如此。我们首先看一下,浏览器每一帧都做了什么。

  1. JavaScript:JavaScript 实现动画效果,DOM 元素操作等。
  2. Style(计算样式):确定每个 DOM 元素应该应用什么 CSS 规则。
  3. Layout(布局):计算每个 DOM 元素在最终屏幕上显示的大小和位置。由于 web 页面的元素布局是相对的,所以其中任意一个元素的位置发生变化,都会联动的引起其他元素发生变化,这个过程叫 reflow。
  4. Paint(绘制):在多个层上绘制 DOM 元素的的文字、颜色、图像、边框和阴影等。
  5. Composite(渲染层合并):按照合理的顺序合并图层然后显示到屏幕上。

动画与图层

浏览器在获取 render tree 后,渲染树中包含了大量的渲染元素,每一个渲染元素会被分到一个图层中,每个图层又会被加载到 GPU 形成渲染纹理。GPU 中 transform 是不会触发 repaint 的,这一点非常类似 3D 绘图功能,最终这些使用 transform 的图层都会由独立的合成器进程进行处理。

过程如下:render tree -> 渲染元素 -> 图层 -> GPU 渲染 -> 浏览器复合图层 -> 生成最终的屏幕图像。

注意: chrome devtools 中可以开启 Rendering 中的 Layer borders 查看图层纹理。
其中黄色边框表示该元素有 3d 变换,表示放到一个新的复合层(composited layer)中渲染,蓝色栅格表示正常的 render layer。

在 GPU 渲染的过程中,一些元素会因为符合了某些规则,而被提升为独立的层(黄色边框部分),一旦独立出来,就不会影响其它 DOM 的布局,所以我们可以利用这些规则,将经常变换的 DOM 主动提升到独立的层,那么在浏览器的一帧运行中,就可以减少 Layout 和 Paint 的时间了

合成渲染

合成渲染,听着可能有些陌生,但是你肯定用过。对于 transform/opacity 这两种变换,浏览器不会用 repaint/reflow 处理,而是在已经渲染好的元素基础上进行附加工作。例如一个黑底色的 div,往右飞 100px, 传统 JS 过程是对每次修改 left 值后重新画一个 div。而如果我们用 transform:translate(0,100px) ,transition:2s 浏览器则是把这个绘制好的 div 单独放在一个画面层再平移这个层过去,div 的几何形状,颜色不会再重复计算,而是保留在这个图层中。Google 开发者的一篇文章介绍了合成渲染的好处,其中有图描述了理想动画效果的流程

性能最佳的像素管道版本会避免布局和绘制,只需要合成更改:为了实现此目标,需要坚持更改可以由合成器单独处理的属性。目前只有两个属性符合条件:transformsopacity

使用 transformopacity 时要注意的是,您更改这些属性所在的元素应处于其自身的合成器层。要做一个层,您必须提升元素,后面我们将介绍方法。

如果没有创建独立的图层,会在动画的过程中先重绘一次,但是依然不会有回流

由于 transform 是位于 Composite Layers 层,而 widthleftmargin 等则是位于 Layout 层,在 Layout 层发生的改变必定导致 Paint Setup and Paint -> Composite Layers,所以相对而言使用 transform 实现的动画效果肯定比 left 这些更加流畅。

而且就算抛开这一角度,在另一方面浏览器也会针对 transform 等开启 GPU 加速。

创建独立图层

Css

如果用的是 2D 转换,并不是 3D 转换。会在动画开始和结束的时候会有两次重绘产生。

3D 转换和 2D 转换的不同在于是否提前生成新的层,如果是 2D 的话是在实行的时候,在动画开始的时候,一个新的层被创建,并且被传入 GPU 处理。当动画结束,独立的层被移除,结果被重新绘制。

即使是 3D 转换,也必须在动画前就存在,才会创建独立图层,否则还是在原始图层,等到动画开始再换成新图层

可以 通过 layout border 查看 图层 黄色边框

实践中并没有触发

其他

Dom

浏览器有可能给复合层之后的所有相对或绝对定位的元素都创建一个复合层来渲染,于是就有了上面我厂项目截图的那种效果。

不过也不是所有浏览器都有这个问题,我在 mac 上的 Safari、firefox 都没有明显差异,安卓手机上的 QQ 浏览器好像也正常,猎豹、UC、欧朋、webview 等浏览器差距明显,更多测试就靠大家来发现吧。

关于 z-index 导致的硬件加速的问题,可以查看这篇文章 http://www.th7.cn/web/html-css/201509/121970.shtml

注意

开启 GPU 加速

如果有一些元素不需要用到上述属性,但是需要触发硬件加速效果,可以使用一些小技巧来诱导浏览器开启硬件加速。

  .element {
      -webkit-transform: translateZ(0);
      -moz-transform: translateZ(0);
      -ms-transform: translateZ(0);
      -o-transform: translateZ(0);
      transform: translateZ(0); 
      /**或者**/
      transform: rotateZ(360deg);
      transform: translate3d(0, 0, 0);
  }
  .example2 {
    transform: rotateZ(360deg);
  }

注意:我在不同的资料中查到的 transform 是否能触发硬件加速的结果不同,自己测试后,发现结果是可以。

因为 opacity 如果不是动画无法新建图层,而且 filter 属性支持度太差,稳健的开启方法只有一个

有必要使用 transform hack 的地方是提高性能。浏览器自身也提供了优化的功能,这也就是 will-change 属性。这个功能允许你告诉浏览器这个属性会发生变化,因此浏览器会在开始之前对其进行优化。这里有一个例子:

  .example {
    will-change: transform;
  }

属性值必须要与发生动画的 CSS 属性匹配,否则还是会导致重绘

要注意的问题

Memory

过多地开启硬件加速可能会耗费较多的内存,因此什么时候开启硬件加速,给多少元素开启硬件加速,需要用测试结果说话。

Font Rendering

GPU 渲染会影响字体的抗锯齿效果。这是因为 GPU 和 CPU 具有不同的渲染机制,即使最终硬件加速停止了,文本还是会在动画期间显示得很模糊。

因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。

Z-index

使用 3D 硬件加速提升动画性能时,最好给元素增加一个 z-index 属性,人为干扰复合层的排序,可以有效减少 chrome 创建不必要的复合层,提升渲染性能,移动端优化效果尤为明显。

进阶

http://velocity.oreilly.com.cn/2013/ppts/16_ms_optimization--web_front-end_performance_optimization.pdf

Experience

#programming-experience

重绘和回流