回流(重构/重排/reflow)
DOM树和CSSOM树结合后的渲染树后,将可见DOM节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流。
重绘(repaint或redraw)
通过构造渲染树和回流阶段,我们知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置、大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点。
回流一定会触发重绘,而重绘不一定会回流
例如background-color等不关于页面结构布局的变化就不会产生回流
浏览器的优化机制
现代的浏览器都是很聪明的,由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。但是!当你获取布局信息的操作的时候,会强制队列刷新,比如当你访问以下属性或者使用以下方法:
- offsetTop、offsetLeft、offsetWidth、offsetHeight
- scrollTop、scrollLeft、scrollWidth、scrollHeight
- clientTop、clientLeft、clientWidth、clientHeight
- getComputedStyle()
- getBoundingClientRect
- 具体可以访问这个网站:gist.github.com/paulirish/5…
以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新渲染队列。
如何减少回流和重绘
1. 尽可能在DOM树的最末端改变class
回流可以自上而下,或自下而上的回流的信息传递给周围的节点。回流是不可避免的,但可以减少其影响。尽可能在DOM树的里面改变class,可以限制了回流的范围,使其影响尽可能少的节点。例如,你应该避免通过改变对包装元素类去影响子节点的显示。面向对象的CSS始终尝试获得它们影响的类对象(DOM节点或节点),但在这种情况下,它已尽可能的减少了回流的影响,增加性能优势。
2. 避免设置多层内联样式
我们都知道与DOM交互很慢。我们尝试在一种无形的DOM树片段组进行更改,然后整个改变应用到DOM上时仅导致了一个回流。同样,通过style属性设置样式导致回流。避免设置多级内联样式,因为每个都会造成回流,样式应该合并在一个外部类,这样当该元素的class属性可被操控时仅会产生一个reflow。
3. 动画效果应用到position属性为absolute或fixed的元素上
动画效果应用到position属性为absolute或fixed的元素上,它们不影响其他元素的布局,所它他们只会导致重新绘制,而不是一个完整回流。这样消耗会更低。
4. 牺牲平滑度换取速度
Opera还建议我们牺牲平滑度换取速度,其意思是指您可能想每次1像素移动一个动画,但是如果此动画及随后的回流使用了100%的CPU,动画就会看上去是跳动的,因为浏览器正在与更新回流做斗争。动画元素每次移动3像素可能在非常快的机器上看起来平滑度低了,但它不会导致CPU在较慢的机器和移动设备中抖动。
5. 避免使用table布局
避免使用table布局。可能您需要其它些避免使用table的理由,在布局完全建立之前,table经常需要多个关口,因为table是个和罕见的可以影响在它们之前已经进入的DOM元素的显示的元素。想象一下,因为表格最后一个单元格的内容过宽而导致纵列大小完全改变。这就是为什么所有的浏览器都逐步地不支持table表格的渲染(感谢Bill Scott提供)。然而有另外一个原因为什么表格布局时很糟糕的主意,根据Mozilla,即使一些小的变化将导致表格(table)中的所有其他节点回流。
6. 避免使用CSS的JavaScript表达式
这项规则较过时,但确实是个好的主意。主要的原因,这些表现是如此昂贵,是因为他们每次重新计算文档,或部分文档、回流。正如我们从所有的很多事情看到的:引发回流,它可以每秒产生成千上万次。当心!
即使使用到的时候也要避免触发同步布局事件
上文我们说过,当我们访问元素的一些属性的时候,会导致浏览器强制清空队列,进行强制同步布局。举个例子,比如说我们想将一个p标签数组的宽度赋值为一个元素的宽度,我们可能写出这样的代码:
function initP() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px';
}
}
这段代码看上去是没有什么问题,可是其实会造成很大的性能问题。在每次循环的时候,都读取了box的一个offsetWidth属性值,然后利用它来更新p标签的width属性。这就导致了每一次循环的时候,浏览器都必须先使上一次循环中的样式更新操作生效,才能响应本次循环的样式读取操作。每一次循环都会强制浏览器刷新队列。我们可以优化为:
const width = box.offsetWidth;
function initP() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = width + 'px';
}
}
参考文章
https://juejin.im/post/5c6cb7b4f265da2dae511a3d
https://blog.youkuaiyun.com/oscar999/article/details/38379523