如何减少浏览器repaint和reflow

vr969956 13年前
   <h2>一、什么是repaint/reflow?</h2>    <p>页面在加载的过程中,需要对文档结构进行解析,同时需要结合各种各样的样式来计算这个页面长什么样子,最后再经过浏览器的渲染页面就出现了。这整个过程细说起来还是比较复杂,其中充满了repaint和reflow。对于DOM结构中的各个元素都有自己的盒子(模型),这些都需要浏览器根据各种样式(浏览器的、开发人员定义的等)来计算并根据计算结果将元素放到它该出现的位置,这个过程称之为reflow;当各种盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来后,浏览器于是便把这些元素都按照各自的特性绘制了一遍,于是页面的内容出现了,这个过程称之为repaint。</p>    <p>以上提到的只是在页面加载时必然会出现的repaint和reflow,除此之外,在页面加载完成后,用户的一些操作、脚本的一些操作都会导致浏览器发生这种行为,具体在后文阐述。</p>    <p>另外,关于浏览器渲染的更为详细的资料可以参考以下,涵盖了IE以及Firefox:</p>    <p><a href="/misc/goto?guid=4959673512428839570">Understanding Internet Explorer Rendering Behaviour</a></p>    <p><a href="/misc/goto?guid=4959673512523905565">Notes on HTML Reflow</a></p>    <p> </p>    <h2>二、什么情况下会触发浏览器的repaint/reflow?</h2>    <p>除了页面在首次加载时必然要经历该过程之外,还有以下行为会触发这个行为:</p>    <ul>     <li>DOM元素的添加、修改(内容)、删除( Reflow + Repaint)</li>     <li>仅修改DOM元素的字体颜色(只有Repaint,因为不需要调整布局)</li>     <li>应用新的样式或者修改任何影响元素外观的属性</li>     <li>Resize浏览器窗口、滚动页面</li>     <li>读取元素的某些属性(offsetLeft、offsetTop、offsetHeight、offsetWidth、scrollTop/Left/Width/Height、clientTop/Left/Width/Height、getComputedStyle()、currentStyle(in IE))</li>    </ul>    <p>在继续下面的文章之前,先介绍一款强大的性能分析工具-<a href="/misc/goto?guid=4959673512598079710">dynaTrace</a>,借助该功能能够清晰的得到页面中的资源消耗情况,从而对症下药。另外,更细节的方面是它可以跟踪每个函数调用所造成的CPU消耗、Repaint/Reflow。接下来就借助该工具来测试一下以上描述的几点情况。</p>    <p> </p>    <h3>DOM元素的增删改</h3>    <p>先看代码</p>    <p>HTML:</p>    <p>这里是第1个节点</p>    <p>这里是第2个节点</p>    <p>这里是第3个节点</p>    <p>Javascript:</p>    <pre>  function $(id){ return document.getElementById(id); } function addNode(){ var n = document.createElement('div'); n.innerHTML = 'New Node'; $('test1').appendChild(n); } function modNode(){ $('test2').innerHTML = 'hello'; } function delNode(){ $('test3').parentNode.removeChild($('test3')); }</pre>    <p>在依次点击完每一个按钮后,我们来看看dynaTrace的情况,首先是一目了然的点击事件分布</p>    <p><img alt="如何减少浏览器repaint和reflow" src="https://simg.open-open.com/show/af40da2e2628bec9ff457c9076ecc482.png" width="420" height="124"></p>    <p> </p>    <p>放大之后来看一下每个事件的repaint/reflow情况:</p>    <p> </p>    <p>增加节点:</p>    <p><img alt="如何减少浏览器repaint和reflow" src="https://simg.open-open.com/show/2e2449311be391b9fc3b719b740bc638.png" width="640" height="127"></p>    <p> </p>    <p>修改节点:</p>    <p><img alt="如何减少浏览器repaint和reflow" src="https://simg.open-open.com/show/d0b97c36811bdc7d30e77e8f25232b8e.png" width="632" height="125"></p>    <p>删除节点:</p>    <p><img alt="如何减少浏览器repaint和reflow" src="https://simg.open-open.com/show/68ca2693374bc19b7c4812535d690e66.png" width="598" height="128"></p>    <p>图中的绿色部分表示的是reflow和repaint过程,其中比较短的绿条标示的reflow过程,后面长条部分表示的是repaint过程。从图中可以看出,对DOM节点的增删改都会造成reflow和repaint,由于改动小所以reflow消耗的时间很短,但是由于repaint是全局的,因此消耗的时间都比较长。</p>    <p> </p>    <h3>修改DOM元素前景色</h3>    <pre>  var n = $('colorNode'); n.style.color = 'red';</pre>    <p> </p>    <p> </p>    <p><img alt="如何减少浏览器repaint和reflow" src="https://simg.open-open.com/show/8863eb537682398d0991aeb16c283b73.png" width="304" height="130"></p>    <p> </p>    <p>从上图中可以看到修改字体颜色后,浏览器只有repaint而没有reflow。接下来试试修改背景色:</p>    <pre>  var n = $('colorNode'); n.style.backgroundColor = 'red';</pre>    <p><img alt="如何减少浏览器repaint和reflow" src="https://simg.open-open.com/show/3329f3fc09784b85f2fc6487f388a4f1.png" width="271" height="126"></p>    <p>由图中可以看出,修改背景色也会造成reflow和repaint。另外,经过测试发现,只要是修改元素的cssText属性,不论它的值是什么,都会导致浏览器reflow和repaint,因此在某些时候选择特定的样式属性赋值会有更好的效果。</p>    <h3>Resize浏览器窗口以及拖动滚动条</h3>    <p> </p>    <p><img alt="如何减少浏览器repaint和reflow" src="https://simg.open-open.com/show/45f245aeb59faad7c019e97a4601bf30.png" width="640" height="110"></p>    <p> </p>    <p>测试中的操作如下:缩小浏览器窗口->放大浏览器窗口->拖动页面滚动条至页面底部。从图中可以看到Resize浏览器窗口以及拖动滚动条都会造成浏览器的repaint,而且CPU的消耗也比较大,尤其是拖动滚动条的时候。</p>    <h3>读取Layout属性</h3>    <p>根据各种参考资料中的描述,在用Javascript读取DOM节点的Layout属性(offsetLeft、offsetTop、offsetHeight、offsetWidth、scrollTop/Left/Width/Height、clientTop/Left/Width/Height、getComputedStyle()、currentStyle(in IE)) 的时候也会触发repaint,不过在以下的测试例子中并没有发现这一点。</p>    <pre>  var n = $('colorNode'); var temp = document.documentElement.currentStyle; temp = n.offsetTop; temp = n.offsetLeft; temp = n.offsetWidth; temp = n.offsetHeight; temp = n.scrollTop; temp = n.scrollHeight; alert(temp);</pre>    <p> </p>    <p><img alt="如何减少浏览器repaint和reflow" src="https://simg.open-open.com/show/ee8bc4708a4656f80f585069221fc4a4.png" width="285" height="145"></p>    <h2>三、浏览器优化</h2>    <p>浏览器对于每一个渲染动作并不是立即执行,而是维护了一个渲染任务队列,浏览器会根据具体的需要分批集中执行其中的任务。除了浏览器自身维护的定期调度之外,脚本中的某些操作会导致浏览器立即执行渲染任务,例如读取元素的Layout属性。</p>    <pre>  var bodystyle = document.body.style; var computed; if (document.body.currentStyle) { computed = document.body.currentStyle; } else { computed = document.defaultView.getComputedStyle(document.body, ''); } //每次都读取 bodystyle.color = 'red'; bodystyle.padding = '1px'; tmp = computed.backgroundColor; bodystyle.color = 'white'; bodystyle.padding = '2px'; tmp = computed.backgroundImage; bodystyle.color = 'green'; bodystyle.padding = '3px'; tmp = computed.backgroundAttachment; //最后再读取 bodystyle.color = 'yellow'; bodystyle.padding = '4px'; bodystyle.color = 'pink'; bodystyle.padding = '5px'; bodystyle.color = 'blue'; bodystyle.padding = '6px'; tmp = computed.backgroundColor; tmp = computed.backgroundImage; tmp = computed.backgroundAttachment;</pre>    <p>每次读取的渲染图:</p>    <p> </p>    <p> </p>    <p><img alt="如何减少浏览器repaint和reflow" src="https://simg.open-open.com/show/3dffe0225f1e155714961d696e15ca12.png" width="640" height="132"></p>    <p> </p>    <p>最后读取的渲染图:</p>    <p> </p>    <p><img alt="如何减少浏览器repaint和reflow" src="https://simg.open-open.com/show/29e7216eff5b63d8d2b7b3090c9c398b.png" width="640" height="128"></p>    <p> </p>    <h2>四、如何优化你的脚本来减少reflow/repaint?</h2>    <p>1. 避免在document上直接进行频繁的DOM操作,如果确实需要可以采用off-document的方式进行,具体的方法包括但不完全包括以下几种:</p>    <blockquote>     <p>(1). 先将元素从document中删除,完成修改后再把元素放回原来的位置</p>     <p>(2). 将元素的display设置为”none”,完成修改后再把display修改为原来的值</p>     <p>(3). 如果需要创建多个DOM节点,可以使用DocumentFragment创建完后一次性的加入document</p>    </blockquote>    <pre>  function appendEveryTime(){ for( var i = 5000; i--; ){ var n = document.createElement('div'); n.innerHTML = 'node ' + i; document.body.appendChild(n);/*每次创建的新节点都append到文档*/ } } function appendLast(){ var frag = document.createDocumentFragment(); for( var i = 5000; i--; ){ var n = document.createElement('div'); n.innerHTML = 'node ' + i; frag.appendChild(n);/*每次创建的节点先放入DocumentFragment中*/ } document.body.appendChild(frag); }</pre>    <p>用dynaTrace观察的结果如下,appendLast的性能无论是在Javascript的执行时间以及浏览器渲染时间方面都优于appendEveryTime。</p>    <p>appendEveryTime:</p>    <p> </p>    <p> </p>    <p><img alt="如何减少浏览器repaint和reflow" src="https://simg.open-open.com/show/9e3db9362693e4204bafc9b6d34876e9.png" width="640" height="84"></p>    <p> </p>    <p>appendLast:</p>    <p> </p>    <p><img alt="如何减少浏览器repaint和reflow" src="https://simg.open-open.com/show/25468b8b3b9cfcdab7a5da669606a4e4.png" width="640" height="88"></p>    <p> </p>    <p> </p>    <p>2. 集中修改样式</p>    <blockquote>     <p>(1). 尽可能少的修改元素style上的属性</p>     <p>(2). 尽量通过修改className来修改样式</p>     <p>(3). 通过cssText属性来设置样式值</p>    </blockquote>    <p>如下的代码中,每一次赋值都会造成浏览器重新渲染,可以采用cssText或者className的方式</p>    <pre>  el.style.color = 'red; el.style.height = '100px'; el.style.fontSize = '12px'; el.style.backgroundColor = 'white';</pre>    <p>3. 缓存Layout属性值</p>    <p>对于Layout属性中非引用类型的值(数字型),如果需要多次访问则可以在一次访问时先存储到局部变量中,之后都使用局部变量,这样可以避免每次读取属性时造成浏览器的渲染。</p>    <pre>  var width = el.offsetWidth; var scrollLeft = el.scrollLeft;</pre>    <p>4. 设置元素的position为absolute或fixed</p>    <p>在元素的position为static和relative时,元素处于DOM树结构当中,当对元素的某个操作需要重新渲染时,浏览器会渲染整个页面。将元素的position设置为absolute和fixed可以使元素从DOM树结构中脱离出来独立的存在,而浏览器在需要渲染时只需要渲染该元素以及位于该元素下方的元素,从而在某种程度上缩短浏览器渲染时间,这在当今越来越多的Javascript动画方面尤其值得考虑。</p>    <p>HTML代码:</p>    <p>Animation Here</p>    <p>Javascript代码:</p>    <pre>  var t = $('test'); ~function(){ t.style.left = t.offsetLeft + 5 + 'px'; t.style.height = t.offsetHeight + 5 + 'px'; setTimeout(arguments.callee,500); }();</pre>    <p>通过修改#test元素的postion为relative和postion分别得到如下两个测试结果</p>    <p>position: relative</p>    <p> </p>    <p> </p>    <p><img alt="如何减少浏览器repaint和reflow" src="https://simg.open-open.com/show/c54043b12c3312af7bec517aead0a704.png" width="640" height="70"></p>    <p> </p>    <p>position: absolute</p>    <p> </p>    <p> </p>    <p><img alt="如何减少浏览器repaint和reflow" src="https://simg.open-open.com/show/aeaf3bff3d31feda66ae5188b620b593.png" width="640" height="90"></p>    <p>在postion:relative的测试当中,浏览器在重新渲染时做的工作比position:absolute多了不少。</p>    <p> </p>    <p>来自:http://www.cnblogs.com/fullhouse/archive/2012/02/20/2360301.html</p>    <p> </p>