重绘 和 回流, 以及如何优化

2,334 阅读4分钟

前言

重绘和回流是老生常谈的问题了,也是面试中经常能被问到的一个问题,小到页面中某一个dom元素样式发生改变,到输入url到浏览器中页面的展现,都离不开它们,今天也就聊一聊它们,顺带巩固一下自己的知识。

浏览器的渲染进程

当浏览器进程把请求到的响应体数据交给渲染进程,通过解析HTML生成DOM树,解析CSS生成CSSOM树,接着从DOM树的根节点开始遍历,找到DOM树中每一个可见的节点(像script,link,meta标签就不可见,以及css的display属性设置为none的节点也不可见),对于每一个可见的节点,从CSSOM树中找到相应的样式,最后根据每一个可见节点和对应的css样式组合生成渲染树。

回流(layout) 和 重绘(painting)

当渲染树生成之后,此时需要计算得到渲染树中每一节点在设备视口对应的具体位置和大小,而这个计算的过程就是回流, 在这个过程完成之后,通过渲染树中每一个节点以及样式,还有它们相对于设备视口的几何信息,此时通过重绘,我们就能得到节点的绝对像素。最后将像素发送给GUI,一个页面就形成了。 总结一下:

  • Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)。
  • Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素。

何时发生重绘和回流?

  • 重绘:当页面元素的样式发生变化,但不影响该节点在文档流的几何信息时,比如color

  • 回流:当页面元素的几何属性或布局发生变化时,比如增删DOM元素,元素的位置发生变化,元素的尺寸发生变化(内外边距,边框,宽高等等),内容发生变化,比如文字发生变化,图片被另一张不同尺寸的图片所替换,浏览器窗口大小发生变化

回流一定会引发重绘,重绘不一定引发回流

浏览器队列优化机制

现代浏览器不会对每次回流重绘都进行操作,而是把涉及回流重绘的操作放到一个队列里面,当一段时间后或者队列放满了后,浏览器会一次性将队列中所有的操作执行,这样就会大大的减少回流重绘的次数,提高性能,但是当代码执行过程中需要获取某些元素的位置信息(比如执行到一些js自带获取位置信息的api时),该队列会被强制刷新,这些api有:offsetLeft,offsetTop, offsetWidth, offsetHeight, clientWidth, clientHeigth,clientLeft,clientTop,scrollWidth, scrollHeight, scrollLeft, scrollTop, getBoundingClientRect()...

如何减少回流和重绘

css

  • 使用transform来代替top(css硬件加速,相同效果前者少一个layout延时)
  • 常见的触发硬件加速的css属性:transform,opacity,filters,Will-change
  • 避免使用table布局
  • 尽量避免多层嵌套,结构尽量扁平化
  • 对于有复杂动画效果的DOM元素应该将其独立处出来,脱离文档流
  • 使用visibility,opacity 代替 display: none (前者的展现只涉及重绘, 后者需要回流)

js

  • 给一个DOM元素设置css样式避免一行一行单独设置(如这样: el.style.width = '40px', el.style.color = 'red'), 可以用cssText进行集中设置,或者先把样式类在css设置好,通过js直接加类名。
  • 涉及批量修改DOM可分为三个步骤 (1): 使要修改的DOM元素脱离文档流。(2): 对其进行多次修改。(3): 将DOM元素带回文档流。
  • 针对上述的涉及批量修改DOM的方法大概有种如下: (1)先将元素的display设置为none,然后修改,最后将其重新显示

(2)使用文档片段(createDocumentfragment())在当前文档流外构建一个子树,操作完再将其插入回文档(一般用来涉及DOM元素的增加)

(3)将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素(api: 拷贝 cloneNode(), 替换 replaceChild())

最后一点

如果要在循环里面调用强制刷新队列的api时,不妨在循环体外用一个变量获取保存,然后在循环体内操作该变量,这样就避免了每次循环都回流一次