DOM操做優化

文檔對象模型(DOM)是一個獨立 於特定語言的應用程序接口。在瀏覽器中,DOM接口是以JavaScript語言實現的,經過JavaScript來操做瀏覽器頁面中的元素,這使得 DOM成爲了JavaScript中重要的組成部分。在富客戶端網頁應用中,界面上UI的更改都是經過DOM操做實現的,並非經過傳統的刷新頁面實現 的。儘管DOM提供了豐富接口供外部調用,但DOM操做的代價很高,頁面前端代碼的性能瓶頸也大多集中在DOM操做上,因此前端性能優化的一個主要的關注 點就是DOM操做的優化。DOM操做優化的總原則是儘可能減小DOM操做。javascript

在討論DOM操做的最佳性能實踐以前,先來看看DOM操做爲何會影響性能。在瀏覽器中,DOM的實現和ECMAScript的實現是分離的。好比 在IE中,ECMAScrit的實如今jscript.dll中,而DOM的實如今mshtml.dll中;在Chrome中使用WebKit中的 WebCore處理DOM和渲染,但ECMAScript是在V8引擎中實現的,其餘瀏覽器的狀況相似。因此經過JavaScript代碼調用DOM接 口,至關於兩個獨立模塊的交互。相比較在同一模塊中的調用,這種跨模塊的調用其性能損耗是很高的。但DOM操做對性能影響最大其實仍是由於它致使了瀏覽器 的重繪(repaint)和重排(reflow)。css

爲了讓你們能更深入地理解重繪和重排對性能的影響,這裏須要簡單敘述一下瀏覽器的渲染原理(若是想詳細瞭解瀏覽器的工做原理,請參照文章《瀏覽器的工做原理:新式網絡瀏覽器幕後揭祕》 )。從下載文檔到渲染頁面的過程當中,瀏覽器會經過解析HTML文檔來構建DOM樹,解析CSS產生CSS規則樹。JavaScript代碼在解析過程當中, 可能會修改生成的DOM樹和CSS規則樹。以後根據DOM樹和CSS規則樹構建渲染樹,在這個過程當中CSS會根據選擇器匹配HTML元素。渲染樹包括了每 個元素的大小、邊距等樣式屬性,渲染樹中不包含隱藏元素及head元素等不可見元素。最後瀏覽器根據元素的座標和大小來計算每一個元素的位置,並繪製這些元 素到頁面上。重繪指的是頁面的某些部分要從新繪製,好比顏色或背景色的修改,元素的位置和尺寸並沒用改變;重排則是元素的位置或尺寸發生了改變,瀏覽器需 要從新計算渲染樹,致使渲染樹的一部分或所有發生變化。渲染樹從新創建後,瀏覽器會從新繪製頁面上受影響的元素。重排的代價比重繪的代價高不少,重繪會影 響部分的元素,而重排則有可能影響所有的元素。以下的這些DOM操做會致使重繪或重排:html

  • 增長、刪除和修改可見DOM元素前端

  • 頁面初始化的渲染html5

  • 移動DOM元素java

  • 修改CSS樣式,改變DOM元素的尺寸node

  • DOM元素內容改變,使得尺寸被撐大瀏覽器

  • 瀏覽器窗口尺寸改變緩存

  • 瀏覽器窗口滾動性能優化

能夠看出,這些操做都是DOM操做中比較常見的。現代瀏覽器會針對重排或重繪作性能優化,好比,把DOM操做積累一批後統一作一次重排或重繪。但在有些狀況下,瀏覽器會當即重排或重繪。好比請求以下的DOM元素佈局信息:offsetTop/Left/Width/HeightscrollTop/Left/Width/HeightclientTop/Left/Width/HeightgetComputedStyle() currentStyle。由於這些值都是動態計算的,因此瀏覽器須要儘快完成頁面的繪製,而後計算返回值,從而打亂了重排或重繪的優化。

DOM操做帶來的頁面重繪或重排是不可避免的,但能夠遵循一些最佳實踐來下降因爲重排或重繪帶來的影響。以下是一些具體的實踐方法:

1. 合併屢次的DOM操做爲單次的DOM操做

最多見頻繁進行DOM操做的是頻繁修改DOM元素的樣式,代碼相似以下:

element.style.borderColor = '#f00';
element.style.borderStyle = 'solid';
element.style.borderWidth = '1px';

這種編碼方式會由於頻繁更改DOM元素的樣式,觸發頁面屢次的重排或重繪,上面介紹過,現代瀏覽器針對這種狀況有性能的優化,它會合並DOM操做,但並非全部的瀏覽器都存在這樣的優化。推薦的方式是把DOM操做盡可能合併,如上的代碼能夠優化爲:

// 優化方案1
element.style.cssText += 'border: 1px solid #f00;';
// 優化方案2
element.className += 'empty';

示例的代碼有兩種優化的方案,都作到了把屢次的樣式設置合併爲一次設置。方案2比方案1稍微有一些性能上的損耗,由於它須要查詢CSS類。但方案2的維護性最好,這在上一章曾經討論過。不少時候,若是性能問題並不突出,選擇編碼方案時須要優先考慮的是代碼的維護性。

相似的操做還有經過innerHTML接口修改DOM元素的內容。不要直接經過此接口來拼接HTML代碼,而是以字符串方式拼接好代碼後,一次性賦值給DOM元素的innerHTML接口。

2. 把DOM元素離線或隱藏後修改

把DOM元素從頁面流中脫離或隱藏,這樣處理後,只會在DOM元素脫離和添加時,或者是隱藏和顯示時纔會形成頁面的重繪或重排,對脫離了頁面佈局流的DOM元素操做就不會致使頁面的性能問題。這種方式適合那些須要大批量修改DOM元素的狀況。具體的方式主要有三種:

(1)使用文檔片斷

文檔片斷是一個輕量級的document對象,並不會和特定的頁面關聯。經過在文檔片斷上進行DOM操做,能夠下降DOM操做對頁面性能的影響,這 種方式是建立一個文檔片斷,並在此片斷上進行必要的DOM操做,操做完成後將它附加在頁面中。對頁面性能的影響只存在於最後把文檔片斷附加到頁面的這一步 操做上。代碼相似以下:

var fragment = document.createDocumentFragment();
// 一些基於fragment的大量DOM操做
...
document.getElementById('myElement').appendChild(fragment);

(2)經過設置DOM元素的display樣式爲none來隱藏元素

這種方式是經過隱藏頁面的DOM元素,達到在頁面中移除元素的效果,通過大量的DOM操做後恢復元素原來的display樣式。對於這類會引發頁面重繪或重排的操做,就只有隱藏和顯示DOM元素這兩個步驟了。代碼相似以下:

var myElement = document.getElementById('myElement');
myElement.style.display = 'none';
// 一些基於myElement的大量DOM操做
...
myElement.style.display = 'block';

(3)克隆DOM元素到內存中

這種方式是把頁面上的DOM元素克隆一份到內存中,而後再在內存中操做克隆的元素,操做完成後使用此克隆元素替換頁面中原來的DOM元素。這樣一來,影響性能的操做就只是最後替換元素的這一步操做了,在內存中操做克隆元素不會引發頁面上的性能損耗。代碼相似以下:

var old = document.getElementById('myElement');
var clone = old.cloneNode(true);
// 一些基於clone的大量DOM操做
...
old.parentNode.replaceChild(clone, old);

在現代的瀏覽器中,由於有了DOM操做的優化,因此應用如上的方式後可能並不能明顯感覺到性能的改善。可是在仍然佔有市場的一些舊瀏覽器中,應用以上這三種編碼方式則能夠大幅提升頁面渲染性能。

3. 設置具備動畫效果的DOM元素的position屬性爲fixed或absolute

把頁面中具備動畫效果的元素設置爲絕對定位,使得元素脫離頁面佈局流,從而避免了頁面頻繁的重排,只涉及動畫元素自身的重排了。這種作法能夠提升動 畫效果的展現性能。若是把動畫元素設置爲絕對定位並不符合設計的要求,則能夠在動畫開始時將其設置爲絕對定位,等動畫結束後恢復原始的定位設置。在不少的 網站中,頁面的頂部會有大幅的廣告展現,通常會動畫展開和摺疊顯示。若是不作性能的優化,這個效果的性能損耗是很明顯的。使用這裏提到的優化方案,則能夠 提升性能。

4. 謹慎取得DOM元素的佈局信息

前面討論過,獲取DOM的佈局信息會有性能的損耗,因此若是存在重複調用,最佳的作法是儘可能把這些值緩存在局部變量中。考慮以下的一個示例:

for (var i=0; i < len; i++) {
    myElements[i].style.top = targetElement.offsetTop + i*5 + 'px';
}

如上的代碼中,會在一個循環中反覆取得一個元素的offsetTop值,事實上,在此代碼中該元素的offsetTop值並不會變動,因此會存在沒必要要的性能損耗。優化的方案是在循環外部取得元素的offsetTop值,相比較以前的方案,此方案只是調用了一遍元素的offsetTop值。更改後的代碼以下:

var targetTop = targetElement.offsetTop;
for (var i=0; i < len; i++) {
    myElements[i].style.top = targetTop+ i*5 + 'px';
}

另外,由於取得DOM元素的佈局信息會強制瀏覽器刷新渲染樹,而且可能會致使頁面的重繪或重排,因此在有大批量DOM操做時,應避免獲取DOM元素 的佈局信息,使得瀏覽器針對大批量DOM操做的優化不被破壞。若是須要這些佈局信息,最好是在DOM操做以前就取得。考慮以下一個示例:

var newWidth = div1.offsetWidth + 10;
div1.style.width = newWidth + 'px';

var newHeight = myElement.offsetHeight + 10; // 強制頁面重排
myElement.style.height = newHeight + 'px'; // 又會重排一次

根據上面的介紹,代碼在遇到取得DOM元素的信息時會觸發頁面從新計算渲染樹,因此如上的代碼會致使頁面重排兩次,若是把取得DOM元素的佈局信息提早,由於瀏覽器會優化連續的DOM操做,因此實際上只會有一次的頁面重排出現,優化後的代碼以下:

var newWidth = div1.offsetWidth + 10;
var newHeight = myElement.offsetHeight + 10;

div1.style.width = newWidth + 'px';
myElement.style.height = newHeight + 'px';

5. 使用事件託管方式綁定事件

在DOM元素上綁定事件會影響頁面的性能,一方面,綁定事件自己會佔用處理時間,另外一方面,瀏覽器保存事件綁定,因此綁定事件也會佔用內存。頁面中 元素綁定的事件越多,佔用的處理時間和內存就越大,性能也就相對越差,因此在頁面中綁定的事件越少越好。一個優雅的手段是使用事件託管方式,即利用事件冒 泡機制,只在父元素上綁定事件處理,用於處理全部子元素的事件,在事件處理函數中根據傳入的參數判斷事件源元素,針對不一樣的源元素作不一樣的處理。這樣就不 須要給每一個子元素都綁定事件了,管理的事件綁定數量變少了,天然性能也就提升了。這種方式也有很大的靈活性,能夠很方便地添加或刪除子元素,不須要考慮因 元素移除或改動而須要修改事件綁定。示例代碼以下:

// 獲取父節點,並添加一個click事件
document.getElementById('list').addEventListener("click",function(e) { // 檢查事件源元素 if(e.target && e.target.nodeName.toUpperCase == "LI") { // 針對子元素的處理 ...
    }
});

上述代碼中,只在父元素上綁定了click事件,當點擊子節點時,click事件會冒泡,父節點捕獲事件後經過e.target檢查事件源元素並作相應地處理。

在JavaScript中,事件綁定方式存在瀏覽器兼容問題,因此在不少框架中也提供了類似的接口方法用於事件託管。好比在jQuery中可使用以下方式實現事件的託管(示例代碼來自jQuery官方網站):

$( "table" ).on( "click", "td", function() { $( this ).toggleClass( "chosen" );
});

原文地址

相關文章
相關標籤/搜索