浏览器从下载文档到显示页面的过程是个复杂的过程,这里包含了重绘和重排。各家浏览器引擎的工作原理略有差别,但也有一定规则。简单讲,通常在文档初次加载时,浏览器引擎会解析HTML文档来构建DOM树,之后根据DOM元素的几何属性构建一棵用于渲染的树。 渲染树的每个节点都有大小和边距等属性,类似于盒子模型(由于隐藏元素不需要显示,渲染树中并不包含DOM树中隐藏的元素)。当渲染树构建完成后,浏览器 就可以将元素放置到正确的位置了,再根据渲染树节点的样式属性绘制出页面。由于浏览器的流布局,对渲染树的计算通常只需要遍历一次就可以完成。但 table及其内部元素除外,它可能需要多次计算才能确定好其在渲染树中节点的属性,通常要花3倍于同等元素的时间。这也是为什么我们要避免使用 table做布局的一个原因。
页面重绘Repaint,页面重排Reflow具体解释如下:
页面重绘Repaint
重绘发生在一个元素的外观被改变,但是整体的布局没有发生变化。比如我们如果只改变了visibility和outline,background,color之类的。
在这个时候浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。重绘不会带来重新布局,并不一定伴随重排。
页面重排Reflow
页面重排发生在我们改变了DOM元素的几何属性,比如我们给一个元素加了border,或者width,height发生了变化,或者我们突然让它float了这些。再比如,我们改变了文字的大小,改变了文字内容。
具体的一些总结如下:
添加或者删除可见的DOM元素
元素位置改变
元素尺寸改变——边距、填充、边框、宽度和高度
内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变;
页面渲染初始化;
浏览器窗口尺寸改变——resize事件发生时;
这些时候浏览器会重新计算元素的属性,会使渲染树中受到影响的部分失效,并且会验证DOM树上的所有节点的visibility属性。这样肯定会造成很大的低效。如果我们重排太过于频繁的话,浏览器肯定就又占用了很多cpu。
刚刚提到了我们重排的时候会验证DOM树上的所有属性,这又是为啥呢?
当DOM元素的几何属性变化时,渲染树中的相关节点就会失效,浏览器会根据DOM元素的变化重建构建渲染树中失效的节点。之后,会根据新的渲染树重新绘 制这部分页面。而且,当前元素的重排也许会带来相关元素的重排。例如,容器节点的渲染树改变时,会触发子节点的重新计算,也会触发其后续兄弟节点的重排, 祖先节点需要重新计算子节点的尺寸也会产生重排。最后,每个元素都将发生重绘。可见,重排一定会引起浏览器的重绘,一个元素的重排通常会带来一系列的反 应,甚至触发整个文档的重排和重绘,性能代价是高昂的。
举个例子以区别重绘和重排
1 | var s = document.body.style; |
如果答对了,就证明已经可以区分重绘和重排了。
再来仔细看下,上面的时候我们把节点插入到了body最后,那么我们为什么不插入到最前面呢?因为插入到后面不一定会影响到前面,但是插入前面就一定会影响到后面。
解决方案?
浏览器自身的优化
从上个实例代码中可以看到几行简单的JS代码就引起了6次左右的回流、重绘。而且我们也知道回流的花销也不小,如果每句JS操作都去回流重绘的话,浏览器可能就会受不了。所以很多浏览器都会优化这些操作,浏览器会维护1个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会flush队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。
但除了渲染树的直接变化,当获取一些属性时,浏览器为取得正确的值也会触发重排。这样就使得浏览器的优化失效了。这些属性包括:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle() (currentStyle in IE)。所以,在多次使用这些值时应进行缓存。
将多次改变样式属性的操作合并成一次操作
比如说把1
2
3
4var changeDiv = document.getElementById('changeDiv');
changeDiv.style.color = '#093';
changeDiv.style.background = '#eee';
changeDiv.style.height = '200px';
变成为:1
2
3
4
5
6div.changeDiv {
background: #eee;
color: #093;
height: 200px;
}
document.getElementById('changeDiv').className = 'changeDiv';
通过这种方式我们也减少了重绘和重排的次数。
让改变的元素不影响别的元素
将需要多次重排的元素,position属性设为absolute或fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。
先拼接html代码
在内存中多次操作节点,完成后再添加到文档中去。例如要异步获取表格数据,渲染到页面。可以先取得数据后在内存中构建整个表格的html片段,再一次性添加到文档中去,而不是循环添加每一行。
缓存到变量
在需要经常取那些引起浏览器重排的属性值时,要缓存到变量。比如说如果用到多次getElementById(“id”)
就把这个变量先存起来。
display:none法
由于display属性为none的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发2次重排。
补充页面的重构
这个跟上面的一样是优化,但是是完全不同的额,我弄错了先前。
代码重构(英语:Code refactoring)重构就是在不改变软件系统外部行为的前提下,改善它的内部结构。比如我们写完网站的时候,就可以重新看下整个网站是不是有很多不必要的标签,或者语义化不是很好。
总结
上面我们知道了 Reflow 有时确实不可避免,所以只能尽可能限制Reflow的影响范围。我觉得最棒的地方在这几种解决方法里,比如说display:none和float方法,我可能不会想到。谢谢这几篇文章:from张鑫旭和from独占神话