回流/重排
当我们对DOM的修改引发了DOM几何尺寸的变化(比如修改元素的宽、高或者隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)
由本身的大小宽高改变,引发局部或者全局的排版,会引发回流或局部回流
- 全局范围:从根节点html开始对整个渲染树进行重新布局
- 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局
重绘
当我们对DOM的修改导致了样式的变化,却未影响其几何属性(比如修改了颜色或者背景色)时,浏览器不需要重新计算元素的几何属性,直接为该元素绘制新的样式,这个过程叫做重绘
渲染树
浏览器渲染流程如下:
- 解析HTML Source,生成DOM树
- 解析CSS,生成CSSOM树
- 将DOM树和CSSOM树结合,去除不可见元素,生成渲染树(Render Tree)
- Layout(布局):根据生成的渲染树,进行布局,得到节点的几何信息(宽度、高度和位置等)
- Painting(重绘):根据渲染树以及回流得到的几何信息,将Render Tree的每个像素渲染到屏幕上
构建渲染树流程:
- 从DOM树的根节点开始遍历每个可见节点
- 对于每个可见节点,找到CSSOM树种对应的规则,并应用它们
- 根据每个可见节点以及其对应样式,组合生成渲染树
不可见节点:
- 一些不会渲染输出的节点,比如
script
,meta
,link
等 - 一些通过css进行隐藏的节点,比如
display:none
,注意,使用visibility
和opacity
隐藏的节点,还是会显示在渲染树上的,只有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,操作结束之后,再添加到文档中,这样只会触发一次重排。或者复制节点,在副本上工作,然后替换它(这样不会有性能问题吗?)