回流和重绘

81 阅读5分钟

渲染流程大致可总结为如下:

  1. 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。

  2. 渲染进程将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。

  3. 创建布局树,并计算元素的布局信息。

  4. 对布局树进行分层,并生成分层树。 image.png 5.为每个图层生成绘制列表,并将其提交到合成线程。

  5. 合成线程将图层分为图块,并在光栅化线程池中将图块转换成位图。

  6. 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。

  7. 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上

其中有三个和渲染流水线相关的概念—— “重排” “重绘” 和 “合成”。

回流/重排(Reflow)

image.png 从上图可以看出,如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重现布局,解析之后的一系列子阶段,这个过程就叫做重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。

哪些操作会导致回流?

以下会导致回流的操作:

页面首次渲染
浏览器窗口大小发生改变
元素尺寸或位置发生改变(如边距、填充、边框、宽度和高度、position、left、top等)
元素内容变化(文字数量或图片大小等)
改变元素字体大小
设置 style 属性的值,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow
dom操作(添加或删除可见的DOM元素等)
css伪类激活
查询某些属性或调用某些方法(如计算元素的offsetWidth、offsetHeight等)
常用且会导致回流的属性和方法:

width、height、margin、padding、border、border-width
display、position、overflow、font-size、vertical-align、min-height
clientWidth/Height/Top/Left
offsetWidth/Height/Top/Left
scrollWidth/Height/Top/Left
scrollIntoView()、scrollIntoViewIfNeeded()
getComputedStyle()、currentStyle(IE获取样式)
getBoundingClientRect()
scrollTo()\

重绘 (Repaint)

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

简单地说,重绘是填充像素的过程。它涉及绘出文本、颜色、图像、边框 和阴影,基本上包括元素的每个可视部分。在重绘阶段,系统会遍历调用渲染对象的 paint 方法,将渲染对象的内容显示在屏幕上。

注意:由页面的渲染过程可知,reflow必将会引起repaint,而repaint不一定会引起reflow。

哪些操作会导致重绘?

color、border-style、visibility、text-decoration
background、outline、box-shadow等

合成

那如果你更改一个既不要布局也不要绘制的属性,会发生什么变化呢?渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成 image.png

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

有两大好处:

能够充分发挥GPU的优势。合成线程生成位图的过程中会调用线程池,并在其中使用GPU进行加速生成,而GPU 是擅长处理位图数据的。

没有占用主线程的资源,即使主线程卡住了,效果依然能够流畅地展示。

避免与优化

CSS

  • 避免使用table布局。
  • 尽可能在DOM树的最末端改变class
  • 避免设置多层内联样式。
  • 将动画效果应用到position属性为absolutefixed的元素上。
  • 避免使用CSS表达式(例如:calc())。

JavaScript

  1. 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
  2. 离线dom:
    • 避免频繁操作DOM,创建一个documentFragment,使用DocumentFragment进行缓存操作,引发一次回流和重绘, 在它上面应用所有DOM操作,最后再把它添加到文档中。
    • 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  3. 缓存布局信息
    • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
    • 例如:
// 不好的写法
for(let i = 0; i < 20; i++ ) { 
    el.style.left = el.offsetLeft + 5 + "px"; 
    el.style.top = el.offsetTop + 5 + "px"; 
}
 
// 可以先缓存起来
var left = el.offsetLeft,  top = el.offsetTop,  style = el.style; 
for (let i = 0; i < 20; i++ ) { 
    left += 5;
    top += 5; 
    style.left = left + "px"; 
    style.top = top + "px"; 
}
  1. 使用 absolute 或 fixed 脱离文档流

    • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
  2. 优化动画

    • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

    • 启用GPU加速

GPU 加速通常包括以下几个部分:Canvas2D,布局合成, CSS3转换(transitions),CSS3 3D变换(transforms),WebGL和视频(video)。

/* 根据上面的结论, 将 2d transform 换成 3d,就可以强制开启 GPU 加速,提高动画性能 */
div {
  transform: translate3d(10px, 10px, 0);
}