回流与重绘

回流/重排

当我们对DOM的修改引发了DOM几何尺寸的变化(比如修改元素的宽、高或者隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)

由本身的大小宽高改变,引发局部或者全局的排版,会引发回流或局部回流

  • 全局范围:从根节点html开始对整个渲染树进行重新布局
  • 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局

重绘

当我们对DOM的修改导致了样式的变化,却未影响其几何属性(比如修改了颜色或者背景色)时,浏览器不需要重新计算元素的几何属性,直接为该元素绘制新的样式,这个过程叫做重绘

渲染树

浏览器渲染流程如下:

  • 解析HTML Source,生成DOM树
  • 解析CSS,生成CSSOM树
  • 将DOM树和CSSOM树结合,去除不可见元素,生成渲染树(Render Tree)
  • Layout(布局):根据生成的渲染树,进行布局,得到节点的几何信息(宽度、高度和位置等)
  • Painting(重绘):根据渲染树以及回流得到的几何信息,将Render Tree的每个像素渲染到屏幕上

渲染树

构建渲染树流程:

  • 从DOM树的根节点开始遍历每个可见节点
  • 对于每个可见节点,找到CSSOM树种对应的规则,并应用它们
  • 根据每个可见节点以及其对应样式,组合生成渲染树

不可见节点:

  • 一些不会渲染输出的节点,比如scriptmetalink
  • 一些通过css进行隐藏的节点,比如display:none,注意,使用visibilityopacity隐藏的节点,还是会显示在渲染树上的,只有display:none的节点才不会显示在渲染树上

CSS异步加载

待续

浏览器的渲染队列

思考以下代码会触发几次渲染?

div.style.left = "10px";
div.style.top = "10px";
div.style.width = "20px";
div.style.height = "20px";

根据上文定义,这段代码理论上会触发4次重排,因为每一次都改变了元素的几何属性,实际上最后只触发了一次重排,这得益于浏览器的渲染队列机制

当我们修改了元素的几何属性,导致浏览器触发重排或者重绘时,它会把该操作放进渲染队列,等到队列中的操作到了一定数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作

div.style.left = "10px";
console.log(div.offsetLeft);
div.style.top = "10px";
console.log(div.offsetTop);
div.style.width = "20px";
console.log(div.offsetWidth);
div.style.height = "20px";
console.log(div.offsetHeight);

这段代码会触发4次重排+重绘,因为在console中可能会用到被修改的style,所以浏览器会立即施加影响,即使该值与操作中修改的值没有关联

优化建议

读写操作分离

div.style.left = "10px";
div.style.top = "10px";
div.style.width = "20px";
div.style.height = "20px";

console.log(div.offsetLeft);
console.log(div.offsetTop);
console.log(div.offsetWidth);
console.log(div.offsetHeight);

样式集中操作

虽然现在大部分浏览器有渲染队列优化,但不排除有些游览器以及老版本的浏览器效率依然低下:建议通过改变class或者cssText属性集中改变样式

// bad
var left = 10;
var top = 10;
el.style.left = left + "px";
el.style.top = top + "px";
// good
el.className += " theclassname";
// good
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
// good
el.style.cssText += `; left:${left}px; top:${top}px;`;

缓存布局信息

// bad
div.style.left = div.offsetLeft + 1 + "px";
div.style.top = div.offsetTop + 1 + "px";

// good 缓存布局信息 相当于读写分离
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
div.style.left = curLeft + 1 + "px";
div.style.top = curTop + 1 + "px";
curLeft = curTop = null;

离线改变DOM

  • 隐藏要操作的 dom 在要操作 dom 之前,通过 display 隐藏 dom,当操作完成之后,才将元素的 display 属性为可见,因为不可见的元素不会触发重排和重绘
dom.display = "none";
// 修改 dom 样式
dom.display = "block";
  • 通过document segment创建一个dom,在其上进行批量操作dom,操作结束之后,再添加到文档中,这样只会触发一次重排。或者复制节点,在副本上工作,然后替换它(这样不会有性能问题吗?)