如何减少浏览器的repaint和reflow?

转自:http://varnow.org/?p=232
参考:http://www.stubbornella.org/content/2009/03/27/reflows-repaints-css-performance-making-your-javascript-slow/

文本内容主要包括以下几点:

   1. 什么是repaint/reflow?
   2. 什么情况下会触发浏览器的repaint/reflow?
   3. 浏览器自身所作的优化
   4. 如何优化你的脚本来减少repaint/reflow?

一、什么是repaint/reflow?

页面在加载的过程中,需要对文档结构进行解析,同时需要结合各种各样的样式来计算这个页面长什么样子,最后再经过浏览器的渲染页面就出现了。这整个过程细说起来还是比较复杂,其中充满了repaint和reflow。对于DOM结构中的各个元素都有自己的盒子(模型),这些都需要浏览器根据各种样式(浏览器的、开发人员定义的等)来计算并根据计算结果将元素放到它该出现的位置,这个过程称之为reflow;当各种盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来后,浏览器于是便把这些元素都按照各自的特性绘制了一遍,于是页面的内容出现了,这个过程称之为repaint。

以上提到的只是在页面加载时必然会出现的repaint和reflow,除此之外,在页面加载完成后,用户的一些操作、脚本的一些操作都会导致浏览器发生这种行为,具体在后文阐述。

另外,关于浏览器渲染的更为详细的资料可以参考以下,涵盖了IE以及Firefox:

Understanding Internet Explorer Rendering Behaviour

Notes on HTML Reflow
二、什么情况下会触发浏览器的repaint/reflow?

除了页面在首次加载时必然要经历该过程之外,还有以下行为会触发这个行为:

    * DOM元素的添加、修改(内容)、删除( Reflow + Repaint)
    * 仅修改DOM元素的字体颜色(只有Repaint,因为不需要调整布局)
    * 应用新的样式或者修改任何影响元素外观的属性
    * Resize浏览器窗口、滚动页面
    * 读取元素的某些属性(offsetLeft、offsetTop、offsetHeight、offsetWidth、 scrollTop/Left/Width/Height、clientTop/Left/Width/Height、 getComputedStyle()、currentStyle(in IE))

在继续下面的文章之前,先介绍一款强大的性能分析工具-dynaTrace,借助该功能能够清晰的得到页面中的资源消耗情况,从而对症下药。另外,更细节的方面是它可以跟踪每个函数调用所造成的CPU消耗、Repaint/Reflow。接下来就借助该工具来测试一下以上描述的几点情况。
DOM元素的增删改

先看代码
view plaincopy to clipboardprint?

   1. <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3c.org/TR/html4/strict.dtd"> 
   2. <html> 
   3.     <body> 
   4.         <div id="test1" onclick="addNode()">这里是第1个节点</div> 
   5.         <div id="test2" onclick="modNode()">这里是第2个节点</div> 
   6.         <div id="test3" onclick="delNode()">这里是第3个节点</div> 
   7.     </body> 
   8.     <script type="text/javascript"> 
   9.         function $(id){ 
  10.             return document.getElementById(id); 
  11.         } 
  12.         function addNode(){ 
  13.             var n = document.createElement('div'); 
  14.             n.innerHTML = 'New Node'; 
  15.             $('test1').appendChild(n); 
  16.         } 
  17.         function modNode(){ 
  18.             $('test2').innerHTML = 'hello'; 
  19.         } 
  20.         function delNode(){ 
  21.             $('test3').parentNode.removeChild($('test3')); 
  22.         } 
  23.     </script> 
  24. </html> 

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3c.org/TR/html4/strict.dtd">
<html>
    <body>
        <div id="test1" onclick="addNode()">这里是第1个节点</div>
        <div id="test2" onclick="modNode()">这里是第2个节点</div>
        <div id="test3" onclick="delNode()">这里是第3个节点</div>
    </body>
    <script type="text/javascript">
        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'));
        }
    </script>
</html>

在依次点击完每一个按钮后,我们来看看dynaTrace的情况,首先是一目了然的点击事件分布

image

放大之后来看一下每个事件的repaint/reflow情况:

增加节点:

image

修改节点:

image

删除节点:

image

图中的绿色部分表示的是reflow和repaint过程,其中比较短的绿条标示的reflow过程,后面长条部分表示的是repaint过程。从图中可以看出,对DOM节点的增删改都会造成reflow和repaint,由于改动小所以reflow消耗的时间很短,但是由于repaint是全局的,因此消耗的时间都比较长。
修改DOM元素前景色
view plaincopy to clipboardprint?

   1. var n = $('colorNode'); 
   2. n.style.color = 'red'; 

var n = $('colorNode');
n.style.color = 'red';

image

从上图中可以看到修改字体颜色后,浏览器只有repaint而没有reflow。接下来试试修改背景色:
view plaincopy to clipboardprint?

   1. var n = $('colorNode'); 
   2. n.style.backgroundColor = 'red'; 

var n = $('colorNode');
n.style.backgroundColor = 'red';

image

由图中可以看出,修改背景色也会造成reflow和repaint。另外,经过测试发现,只要是修改元素的cssText属性,不论它的值是什么,都会导致浏览器reflow和repaint,因此在某些时候选择特定的样式属性赋值会有更好的效果。
Resize浏览器窗口以及拖动滚动条

image

测试中的操作如下:缩小浏览器窗口->放大浏览器窗口->拖动页面滚动条至页面底部。从图中可以看到Resize浏览器窗口以及拖动滚动条都会造成浏览器的repaint,而且CPU的消耗也比较大,尤其是拖动滚动条的时候。
读取Layout属性

根据各种参考资料中的描述,在用Javascript读取DOM节点的Layout属性(offsetLeft、offsetTop、 offsetHeight、offsetWidth、scrollTop/Left/Width/Height、clientTop/Left /Width/Height、getComputedStyle()、currentStyle(in IE)) 的时候也会触发repaint,不过在以下的测试例子中并没有发现这一点。
view plaincopy to clipboardprint?

   1. var n = $('colorNode'); 
   2. var temp = document.documentElement.currentStyle; 
   3. temp = n.offsetTop; 
   4. temp = n.offsetLeft; 
   5. temp = n.offsetWidth; 
   6. temp = n.offsetHeight; 
   7. temp = n.scrollTop; 
   8. temp = n.scrollHeight; 
   9. alert(temp); 

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);

image

三、浏览器优化

浏览器对于每一个渲染动作并不是立即执行,而是维护了一个渲染任务队列,浏览器会根据具体的需要分批集中执行其中的任务。除了浏览器自身维护的定期调度之外,脚本中的某些操作会导致浏览器立即执行渲染任务,例如读取元素的Layout属性。
view plaincopy to clipboardprint?

   1. var bodystyle = document.body.style; 
   2. var computed; 
   3. if (document.body.currentStyle) { 
   4.   computed = document.body.currentStyle; 
   5. } else { 
   6.   computed = document.defaultView.getComputedStyle(document.body, ''); 
   7. } 
   8.  
   9. //每次都读取 
  10.  
  11. bodystyle.color = 'red'; 
  12. bodystyle.padding = '1px'; 
  13. tmp = computed.backgroundColor; 
  14. bodystyle.color = 'white'; 
  15. bodystyle.padding = '2px'; 
  16. tmp = computed.backgroundImage; 
  17. bodystyle.color = 'green'; 
  18. bodystyle.padding = '3px'; 
  19. tmp = computed.backgroundAttachment; 
  20.  
  21. //最后再读取 
  22.  
  23. bodystyle.color = 'yellow'; 
  24. bodystyle.padding = '4px'; 
  25. bodystyle.color = 'pink'; 
  26. bodystyle.padding = '5px'; 
  27. bodystyle.color = 'blue'; 
  28. bodystyle.padding = '6px'; 
  29. tmp = computed.backgroundColor; 
  30. tmp = computed.backgroundImage; 
  31. tmp = computed.backgroundAttachment; 

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;

每次读取的渲染图:

image

最后读取的渲染图:

image
四、如何优化你的脚本来减少reflow/repaint?

1. 避免在document上直接进行频繁的DOM操作,如果确实需要可以采用off-document的方式进行,具体的方法包括但不完全包括以下几种:

    (1). 先将元素从document中删除,完成修改后再把元素放回原来的位置

    (2). 将元素的display设置为”none”,完成修改后再把display修改为原来的值

    (3). 如果需要创建多个DOM节点,可以使用DocumentFragment创建完后一次性的加入document

view plaincopy to clipboardprint?

   1. function appendEveryTime(){ 
   2.     for( var i = 5000; i--; ){ 
   3.         var n = document.createElement('div'); 
   4.         n.innerHTML = 'node ' + i; 
   5.         document.body.appendChild(n);/*每次创建的新节点都append到文档*/ 
   6.     } 
   7. } 
   8.  
   9. function appendLast(){ 
  10.      var frag = document.createDocumentFragment(); 
  11.      for( var i = 5000; i--; ){ 
  12.          var n = document.createElement('div'); 
  13.          n.innerHTML = 'node ' + i; 
  14.          frag.appendChild(n);/*每次创建的节点先放入DocumentFragment中*/ 
  15.      } 
  16.      document.body.appendChild(frag); 
  17. } 

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);
}

用dynaTrace观察的结果如下,appendLast的性能无论是在Javascript的执行时间以及浏览器渲染时间方面都优于 appendEveryTime。

appendEveryTime:

image

appendLast:

image

2. 集中修改样式

    (1). 尽可能少的修改元素style上的属性

    (2). 尽量通过修改className来修改样式

    (3). 通过cssText属性来设置样式值

如下的代码中,每一次赋值都会造成浏览器重新渲染,可以采用cssText或者className的方式
view plaincopy to clipboardprint?

   1. el.style.color = 'red;
   2. el.style.height = '100px'; 
   3. el.style.fontSize = '12px';
   4. el.style.backgroundColor = 'white'; 

el.style.color = 'red;
el.style.height = '100px';
el.style.fontSize = '12px';
el.style.backgroundColor = 'white';

3. 缓存Layout属性值

对于Layout属性中非引用类型的值(数字型),如果需要多次访问则可以在一次访问时先存储到局部变量中,之后都使用局部变量,这样可以避免每次读取属性时造成浏览器的渲染。
view plaincopy to clipboardprint?

   1. var width = el.offsetWidth; 
   2. var scrollLeft = el.scrollLeft; 

var width = el.offsetWidth;
var scrollLeft = el.scrollLeft;

4. 设置元素的position为absolute或fixed

在元素的position为static和relative时,元素处于DOM树结构当中,当对元素的某个操作需要重新渲染时,浏览器会渲染整个页面。将元素的position设置为absolute和fixed可以使元素从DOM树结构中脱离出来独立的存在,而浏览器在需要渲染时只需要渲染该元素以及位于该元素下方的元素,从而在某种程度上缩短浏览器渲染时间,这在当今越来越多的Javascript动画方面尤其值得考虑。
view plaincopy to clipboardprint?

   1. <body style="position:relative"> 
   2.     <div id="test" style="background-color:red;width:100px;position:relative;">Animation Here</div> 
   3. </body> 
   4. <script type="text/javascript"> 
   5.     function $(id){ 
   6.         return document.getElementById(id); 
   7.     } 
   8.     window.onload = function(){ 
   9.         var t = $('test'); 
  10.  
  11.         ~function(){ 
  12.             tt.style.left = t.offsetLeft + 5 + 'px'; 
  13.             tt.style.height = t.offsetHeight + 5 + 'px'; 
  14.             setTimeout(arguments.callee,500); 
  15.         }(); 
  16.     } 
  17. </script> 

<body style="position:relative">
<div id="test" style="background-color:red;width:100px;position:relative;">Animation Here</div>
</body>
<script type="text/javascript">
function $(id){
return document.getElementById(id);
}
window.onload = function(){
var t = $('test');

~function(){
t.style.left = t.offsetLeft + 5 + 'px';
t.style.height = t.offsetHeight + 5 + 'px';
setTimeout(arguments.callee,500);
}();
}
</script>

通过修改#test元素的postion为relative和postion分别得到如下两个测试结果

position: relative

image

position: absolute

image

在postion:relative的测试当中,浏览器在重新渲染时做的工作比position:absolute多了不少。

参考资料

Understanding Internet Explorer Rendering Behaviour

Notes on HTML Reflow

EFFICIENT JAVASCRIPT
WEB优化reflow, repaint, 性能优化

You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

你可能感兴趣的:(JavaScript,浏览器,IE,脚本,firefox)