一、什么是repaint/reflow?
頁面在加載的過程中,需要對文檔結構進行解析,同時需要結合各種各樣的樣式來計算這個頁面長什么樣子,最后再經過瀏覽器的渲染頁面就出現了。這整個過程細說起來還是比較復雜,其中充滿了repaint和reflow。對於DOM結構中的各個元素都有自己的盒子(模型),這些都需要瀏覽器根據各種樣式(瀏覽器的、開發人員定義的等)來計算並根據計算結果將元素放到它該出現的位置,這個過程稱之為reflow;當各種盒子的位置、大小以及其他屬性,例如顏色、字體大小等都確定下來后,瀏覽器於是便把這些元素都按照各自的特性繪制了一遍,於是頁面的內容出現了,這個過程稱之為repaint。
以上提到的只是在頁面加載時必然會出現的repaint和reflow,除此之外,在頁面加載完成后,用戶的一些操作、腳本的一些操作都會導致瀏覽器發生這種行為,具體在后文闡述。
另外,關於瀏覽器渲染的更為詳細的資料可以參考以下,涵蓋了IE以及Firefox:
Understanding Internet Explorer Rendering Behaviour
二、什么情況下會觸發瀏覽器的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元素的增刪改
先看代碼
HTML:
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')); }
在依次點擊完每一個按鈕后,我們來看看dynaTrace的情況,首先是一目了然的點擊事件分布
放大之后來看一下每個事件的repaint/reflow情況:
增加節點:
修改節點:
刪除節點:
圖中的綠色部分表示的是reflow和repaint過程,其中比較短的綠條標示的reflow過程,后面長條部分表示的是repaint過程。從圖中可以看出,對DOM節點的增刪改都會造成reflow和repaint,由於改動小所以reflow消耗的時間很短,但是由於repaint是全局的,因此消耗的時間都比較長。
修改DOM元素前景色
var n = $('colorNode'); n.style.color = 'red';
從上圖中可以看到修改字體顏色后,瀏覽器只有repaint而沒有reflow。接下來試試修改背景色:
var n = $('colorNode'); n.style.backgroundColor = 'red';
由圖中可以看出,修改背景色也會造成reflow和repaint。另外,經過測試發現,只要是修改元素的cssText屬性,不論它的值是什么,都會導致瀏覽器reflow和repaint,因此在某些時候選擇特定的樣式屬性賦值會有更好的效果。
Resize瀏覽器窗口以及拖動滾動條
測試中的操作如下:縮小瀏覽器窗口->放大瀏覽器窗口->拖動頁面滾動條至頁面底部。從圖中可以看到Resize瀏覽器窗口以及拖動滾動條都會造成瀏覽器的repaint,而且CPU的消耗也比較大,尤其是拖動滾動條的時候。
讀取Layout屬性
根據各種參考資料中的描述,在用Javascript讀取DOM節點的Layout屬性(offsetLeft、offsetTop、offsetHeight、offsetWidth、scrollTop/Left/Width/Height、clientTop/Left/Width/Height、getComputedStyle()、currentStyle(in IE)) 的時候也會觸發repaint,不過在以下的測試例子中並沒有發現這一點。
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);
三、瀏覽器優化
瀏覽器對於每一個渲染動作並不是立即執行,而是維護了一個渲染任務隊列,瀏覽器會根據具體的需要分批集中執行其中的任務。除了瀏覽器自身維護的定期調度之外,腳本中的某些操作會導致瀏覽器立即執行渲染任務,例如讀取元素的Layout屬性。
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;
每次讀取的渲染圖:
最后讀取的渲染圖:
四、如何優化你的腳本來減少reflow/repaint?
1. 避免在document上直接進行頻繁的DOM操作,如果確實需要可以采用off-document的方式進行,具體的方法包括但不完全包括以下幾種:
(1). 先將元素從document中刪除,完成修改后再把元素放回原來的位置
(2). 將元素的display設置為”none”,完成修改后再把display修改為原來的值
(3). 如果需要創建多個DOM節點,可以使用DocumentFragment創建完后一次性的加入document
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:
appendLast:
2. 集中修改樣式
(1). 盡可能少的修改元素style上的屬性
(2). 盡量通過修改className來修改樣式
(3). 通過cssText屬性來設置樣式值
如下的代碼中,每一次賦值都會造成瀏覽器重新渲染,可以采用cssText或者className的方式
el.style.color = 'red; el.style.height = '100px'; el.style.fontSize = '12px'; el.style.backgroundColor = 'white';
3. 緩存Layout屬性值
對於Layout屬性中非引用類型的值(數字型),如果需要多次訪問則可以在一次訪問時先存儲到局部變量中,之后都使用局部變量,這樣可以避免每次讀取屬性時造成瀏覽器的渲染。
var width = el.offsetWidth; var scrollLeft = el.scrollLeft;
4. 設置元素的position為absolute或fixed
在元素的position為static和relative時,元素處於DOM樹結構當中,當對元素的某個操作需要重新渲染時,瀏覽器會渲染整個頁面。將元素的position設置為absolute和fixed可以使元素從DOM樹結構中脫離出來獨立的存在,而瀏覽器在需要渲染時只需要渲染該元素以及位於該元素下方的元素,從而在某種程度上縮短瀏覽器渲染時間,這在當今越來越多的Javascript動畫方面尤其值得考慮。
HTML代碼:
Javascript代碼:
var t = $('test'); ~function(){ t.style.left = t.offsetLeft + 5 + 'px'; t.style.height = t.offsetHeight + 5 + 'px'; setTimeout(arguments.callee,500); }();
通過修改#test元素的postion為relative和postion分別得到如下兩個測試結果
position: relative
position: absolute
在postion:relative的測試當中,瀏覽器在重新渲染時做的工作比position:absolute多了不少。