讀古今文學網 > 編寫高質量代碼:改善JavaScript程序的188個建議 > 建議117:減少DOM重繪和重排版次數 >

建議117:減少DOM重繪和重排版次數

瀏覽器在完成所有頁面HTML標記、JavaScript、CSS、圖片下載後,將解析文件並創建兩個內部數據結構。

❑一棵DOM樹:表示頁面結構。

❑一棵渲染樹:表示DOM節點如何顯示。

在渲染樹中為每個需要顯示的DOM樹節點存放至少一個節點(隱藏的DOM元素在渲染樹中沒有對應節點)。將渲染樹上的節點稱為「框」或者「盒」,符合CSS模型的定義,將頁面元素看做一個具有填充、邊距、邊框和位置的盒。一旦DOM樹和渲染樹構造完畢,瀏覽器就可以顯示(繪製)頁面上的元素了。

當DOM改變影響到元素的幾何屬性(寬和高)時,如改變邊框寬度或在段落中添加文字將發生一系列後續動作,瀏覽器需要重新計算元素的幾何屬性,而且其他元素的幾何屬性和位置也會因此改變並受到影響。瀏覽器使渲染樹上受到影響的部分失效,然後重構渲染樹,這個過程稱做重排版。當重排版完成時,瀏覽器會在一個重繪進程中重新繪製屏幕上受影響的部分。

不是所有的DOM改變都會影響幾何屬性。例如,改變一個元素的背景顏色不會影響它的寬度或高度。在這種情況下,只需要重繪(不需要重排版),因為元素的佈局沒有改變。

重繪和重排版是負擔很重的操作,可能導致網頁應用的用戶界面失去響應。因此,應盡可能減少這類事情的發生。當佈局和幾何發生改變時需要重排版。在下述情況中會發生重排版:

❑添加或刪除可見的DOM元素。

❑元素位置改變。

❑元素尺寸改變(因為邊距、填充、邊框寬度、寬度和高度等屬性改變)。

❑內容改變,如文本改變或圖片被另一個不同尺寸的圖片所替代。

❑最初的頁面渲染。

❑瀏覽器窗口改變尺寸。

根據改變的性質,渲染樹上或大或小的一部分需要重新計算。某些改變可能導致重排版整個頁面,如當一個滾動條出現時。

因為計算量與每次重排版有關,因此大多數瀏覽器都通過隊列化修改和批量顯示來優化重排版過程。然而,可能經常不由自主地強迫隊列進行刷新並要求立刻應用所有計劃改變的部分。獲取佈局信息的操作將導致刷新隊列動作,這意味著使用了下面這些方法:

❑offsetTop、offsetLeft、offsetWidth、offsetHeight

❑scrollTop、scrollLeft、scrollWidth、scrollHeight

❑clientTop、clientLeft、clientWidth、clientHeight

❑getComputedStyle(在IE中此函數稱為currentStyle)

佈局信息是由這些方法返回最新的數據,瀏覽器不得不運行渲染隊列中待改變的項目並重新排版以返回正確的值。

在改變樣式的過程中,最好不要使用前面列出的那些屬性。任何一個訪問都將刷新渲染隊列,即使正在獲取那些最近未發生改變的或與最新的改變無關的佈局信息。例如,下面示例改變同一個風格屬性3次。


var computed,tmp=\'\',bodystyle=document.body.style;

if(document.body.currentStyle){//IE,Opera

computed=document.body.currentStyle;

}else{//W3C

computed=document.defaultView.getComputedStyle(document.body,\'\');

}

bodystyle.color=\'red\';

tmp=computed.backgroundColor;

bodystyle.color=\'white\';

tmp=computed.backgroundImage;

bodystyle.color=\'green\';

tmp=computed.backgroundAttachment;


在上面代碼中,body元素的前景色被改變了3次,在每次改變之後都導入了computed的風格。導入的屬性backgroundColor、backgroundImage和backgroundAttachment與顏色改變無關。然而,瀏覽器需要刷新渲染隊列並重排版,因為computed的風格是被查詢而引發的。

更好的方法是不要在佈局信息改變時查詢computed風格。如果將查詢computed風格的代碼移到末尾,那麼在所有瀏覽器上都會執行得更快。


bodystyle.color=\'red\';

bodystyle.color=\'white\';

bodystyle.color=\'green\';

tmp=computed.backgroundColor;

tmp=computed.backgroundImage;

tmp=computed.backgroundAttachment;


由於重排版和重繪代價較高,因此,提高程序響應速度的一個好策略是減少此類操作發生的機會。為減少發生次數,應該將多個DOM和風格改變後合併到一個批次中一次性執行。


var el=document.getElementById(\'myp\');

el.style.borderLeft=\'1px\';

el.style.borderRight=\'2px\';

el.style.padding=\'5px\';


上面代碼中改變了3個樣式屬性,每次改變都影響到元素的幾何屬性,導致瀏覽器重排版了3次。目前大多數瀏覽器都優化了這種情況,只進行一次重排版,但在舊版本瀏覽器中,效率將十分低下。如果其他代碼在這段代碼運行時查詢佈局信息,將導致3次重佈局發生。而且,此代碼訪問DOM 4次,可以被優化。

實現相同效果但效率更高的方法:將所有改變合併在一起執行,只修改DOM一次。具體可通過使用cssText屬性實現:


var el=document.getElementById(\'myp\');

el.style.cssText=\'border-left:1px;border-right:2px;padding:5px;\';


在這個示例中,修改cssText屬性,覆蓋已存在的風格信息。如果打算保持當前的風格,那麼可以將它附加在cssText字符串的後面。


el.style.cssText+=\';border-left:1px;\';


另一個方法是修改CSS的類名稱,而不是修改內聯風格代碼。這種方法適用於那些風格不依賴於運行邏輯且不需要計算的情況。改變後的CSS類名稱更清晰,更易於維護,雖然它可能帶來輕微的性能衝擊,但是有助於保持腳本免除顯示代碼。


var el=document.getElementById(\'myp\');

el.className=\'active\';


當需要對DOM元素進行多次修改時,可以通過以下步驟減少重繪和重排版的次數。

第1步,從文檔流中摘除該元素。

第2步,對其應用多重改變。

第3步,將元素帶回文檔中。

此過程引發兩次重排版:第1步引發一次,第3步引發一次。如果忽略了這兩個步驟,那麼第2步中每次改變都將引發一次重排版。

經歷以下3步後可以將DOM從文檔中摘除:

❑隱藏元素,進行修改,然後再顯示它。

❑使用一個文檔片斷在已存DOM之外創建一個子樹,然後將它複製到文檔中。

❑將原始元素複製到一個脫離文檔的節點中,修改副本,然後覆蓋原始元素。

下面示例中有一個鏈接列表,它必須被更多的信息所更新。


<ul>

<li><a href=\"#\">鏈接1</a></li>

<li><a href=\"#\">鏈接2</a></li>

</ul>


假設附加數據已經存儲在一個對像中了,需要將其插入到這個列表中。這些數據定義如下:


var data=[{

\"name\":\"鏈接3\",

\"url\":\"#\"

},{

\"name\":\"鏈接4\",

\"url\":\"#\"

}];


下面是一個通用的函數,用於將新數據更新到指定節點中:


function appendDataToElement(appendToElement,data){

var a,li;

for(var i=0,max=data.length;i<max;i++){

a=document.createElement(\'a\');

a.href=data[i].url;

a.appendChild(document.createTextNode(data[i].name));

li=document.createElement(\'li\');

li.appendChild(a);

appendToElement.appendChild(li);

}

};


將數據更新到列表而不管重排版問題,最顯著的方法如下:


var ul=document.getElementById(\'mylist\');

appendDataToElement(ul,data);


然而,將data隊列上的每個新條目追加到DOM樹都會導致重排版。第一種減少重排版的方法:改變display屬性,臨時從文檔上移除<ul>元素然後再恢復它。


var ul=document.getElementById(\'mylist\');

ul.style.display=\'none\';

appendDataToElement(ul,data);

ul.style.display=\'block\';


第二種減少重排版的方法:在文檔之外創建並更新一個文檔片斷,然後將它附加在原始列表上。文檔片斷是一個輕量級的document對象,它被設計用於更新、移動節點之類的任務。文檔片斷一個便利的語法特性:在向節點附加一個片斷時,實際添加的是文檔片斷的子節點群,而不是文檔片斷自己。下面的例子減少一行代碼,只引發一次重排版。


var fragment=document.createDocumentFragment;

appendDataToElement(fragment,data);

document.getElementById(\'mylist\').appendChild(fragment);


第三種減少重排版的方法:首先創建要更新節點的副本,然後在副本上操作,最後用新節點覆蓋老節點。


var old=document.getElementById(\'mylist\');

var clone=old.cloneNode(true);

appendDataToElement(clone,data);

old.parentNode.replaceChild(clone,old);


盡可能使用文檔片斷(第二種方法)來減少重排版,因為它涉及最少數量的DOM操作和重排版。唯一潛在的隱患:當前文檔片斷還沒有得到充分利用。

瀏覽器通過隊列化修改和批量運行的方法,盡量減少重排版次數。當查詢佈局信息如偏移量、滾動條位置或風格屬性時,瀏覽器刷新隊列並執行所有修改操作,以返回最新的數值。應盡量減少對佈局信息的查詢,查詢時將查詢次數賦給局部變量,並通過局部變量參與計算。

例如,將元素myElement向右下方向平移,每次一個像素,起始於100像素×100像素位置,結束於500像素×500像素位置,在timeout循環體中可以使用。


myElement.style.left=1+myElement.offsetLeft+\'px\';

myElement.style.top=1+myElement.offsetTop+\'px\';

if(myElement.offsetLeft>=500){

stopAnimation;

}


這樣做很沒有效率,因為每次元素移動,代碼查詢偏移量,就會導致瀏覽器刷新渲染隊列,並不會從優化中獲益。還有一個辦法,只需要獲得起始位置值一次,將它存入局部變量中(var current=myElement.offsetLeft;),然後在動畫循環中使用current變量而不再查詢偏移量。


current++

myElement.style.left=current+\'px\';

myElement.style.top=current+\'px\';

if(current>=500){

stopAnimation;

}


重排版有時只影響渲染樹的一小部分,但也可能影響很大一部分,甚至整個渲染樹。瀏覽器需要重排版的部分越小,應用程序的響應速度就越快,因此,當一個頁面頂部的動畫推移了差不多整個頁面時,將引發巨大的重排版動作,使用戶感到動畫不流暢。渲染樹的大多數節點需要重新計算,這使情況變得更糟糕。

使用以下步驟可以避免對大部分頁面進行重排版:

❑使用絕對坐標定位頁面動畫的元素,使它位於頁面佈局流之外。

❑啟動元素動畫,當它擴大時,將會臨時覆蓋部分頁面。這是一個重繪過程,但只影響頁面的一小部分,避免重排版及重繪一大塊頁面。

❑當動畫結束時,重新定位。