浏览器的重排与重绘

535 阅读5分钟

重排

浏览器下载完页面中的所有组件(HTML、JavaScript、CSS、图片)之后会解析生成两个内部数据结构(DOM树和渲染树),DOM树表示页面结构,渲染树表示DOM节点如何显示。重排是DOM元素的几何属性变化,DOM树的结构变化,渲染树需要重新计算。 ##重绘 重绘是一个元素外观的改变所触发的浏览器行为,例如改变visibility、outline、背景色等属性。浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。由于浏览器的流布局,对渲染树的计算通常只需要遍历一次就可以完成。但table及其内部元素除外,它可能需要多次计算才能确定好其在渲染树中节点的属性值,比同等元素要多花两倍时间,这就是我们尽量避免使用table布局页面的原因之一。 ##重绘和重排的关系 重绘不会引起重排,但重排一定会引起重绘,一个元素的重排通常会带来一系列的反应,甚至触发整个文档的重排和重绘,性能代价是高昂的。

一不小心触发了重排?

(1)页面渲染初始化时;(这个无法避免) (2)浏览器窗口改变尺寸; (3)元素尺寸改变时; (4)元素位置改变时; (5)元素内容改变时; (6)添加或删除可见的DOM 元素时。

重排优化

  1. 将多次改变样式属性的操作合并成一次操作,减少DOM访问。
var changeDiv = document.getElementById(‘changeDiv’);
changeDiv.style.background = ‘#333’;
changeDiv.style.color = ‘#eee′;
changeDiv.style.height = ’200px’;

可以合并为

.changeDiv {
  background: #333;
  color: #eee;
  height: 100px;
}
document.getElementById(‘changeDiv’).className = ‘changeDiv’;
  1. 如果要批量添加DOM,可以先让元素脱离文档流,操作完后再带入文档流,这样只会触发一次重排。(fragment元素的应用)
<ul id='fruit'>
 <li> apple </li>
 <li> orange </li>
</ul>

如果代码中要添加内容为peach、watermelon两个选项,一般会这么做:

var lis = document.getElementById('fruit');
var li = document.createElement('li');
li.innerHTML = 'apple';
lis.appendChild(li);
 
var li = document.createElement('li');
li.innerHTML = 'watermelon';
lis.appendChild(li);

很容易想到如上代码,但是很显然,重排了两次,怎么破?前面我们说了,隐藏的元素不在渲染树中,太棒了,我们可以先把id为fruit的ul元素隐藏(display=none),然后添加li元素,最后再显示,但是实际操作中可能会出现闪动,原因这也很容易理解。这时,fragment元素就有了用武之地了。

var fragment = document.createDocumentFragment();
 
var li = document.createElement('li');
li.innerHTML = 'apple';
fragment.appendChild(li);
 
var li = document.createElement('li');
li.innerHTML = 'watermelon';
fragment.appendChild(li);
 
document.getElementById('fruit').appendChild(fragment);

文档片段是个轻量级的document对象,它的设计初衷就是为了完成这类任务——更新和移动节点。文档片段的一个便利的语法特性是当你附加一个片断到节点时,实际上被添加的是该片断的子节点,而不是片断本身。只触发了一次重排,而且只访问了一次实时的DOM。 3. 使用cloneNode(true or false) 和 replaceChild 技术,引发一次回流和重绘, 在内存中多次操作节点,完成后再添加到文档中去。例如要异步获取表格数据,渲染到页面。可以先取得数据后在内存中构建整个表格的html片段,再一次性添加到文档中去,而不是循环添加每一行。 4. 由于display属性为none的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发两次重排。 5. 将需要多次重排的元素,position属性设为absolute或fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。

聪明的浏览器

也许几行简单的JS代码就会引起了多次回流、重绘。而且我们也知道重排的花销也不小,如果每句JS操作都去重排重绘的话,浏览器可能就会受不了。所以很多浏览器都会优化这些操作,浏览器会维护1个队列,把所有会引起重排、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会flush队列,进行一个批处理。这样就会让多次的重排、重绘变成一次重排重绘。

虽然有了浏览器的优化,但有时候我们写的一些代码可能会强制浏览器提前flush队列,这样浏览器的优化可能就起不到作用了。当你请求向浏览器请求一些 style信息的时候,就会让浏览器flush队列,比如:

  1. offsetTop, offsetLeft, offsetWidth, offsetHeight

  2. scrollTop/Left/Width/Height

  3. clientTop/Left/Width/Height

  4. width,height

  5. 请求了getComputedStyle(), 或者 IE的 currentStyle

当你请求上面的一些属性的时候,浏览器为了给你最精确的值,需要flush队列,因为队列中可能会有影响到这些值的操作。即使你获取元素的布局和样式信息跟最近发生或改变的布局信息无关,浏览器都会强行刷新渲染队列,从而引起多次重排重绘。所以尽量不要在布局信息改变时做查询。

##CSS解析和渲染

  1. CSS(外链或内联)会阻塞 整个 DOM的渲染(Rendering),然而DOM解析(Parsing)会正常进行
  2. 很多浏览器中,CSS会延迟脚本执行和 DOMContentLoaded 事件 JS(外链或内联)会阻塞 后续 DOM的解析(Parsing),后续DOM的渲染(Rendering)也将被阻塞
  3. JS前的DOM可以正常解析(Parsing)和渲染(Rendering)