介面上UI的更改都是通過DOM操作實現的,並不是通過傳統的重新整理頁面實現 的。儘管DOM提供了豐富介面供外部呼叫,但DOM操作的代價很高,頁面前端程式碼的效能瓶頸也大多集中在DOM操作上,所以前端效能優化的一個主要的關注 點就是DOM操作的優化。DOM操作優化的總原則是儘量減少DOM操作。
先來看看DOM操作為什麼會影響效能
在瀏覽器中,DOM的實現和ECMAScript的實現是分離的。比如 在IE中,ECMAScrit的實現在jscript.dll中,而DOM的實現在mshtml.dll中;在Chrome中使用WebKit中的 WebCore處理DOM和渲染,但ECMAScript是在V8引擎中實現的,其他瀏覽器的情況類似。所以通過JavaScript程式碼呼叫DOM接 口,相當於兩個獨立模組的互動。相比較在同一模組中的呼叫,這種跨模組的呼叫其效能損耗是很高的。但DOM操作對效能影響最大其實還是因為它導致了瀏覽器 的重繪(repaint)和迴流(reflow)。
瀏覽器的渲染原理
從下載文件到渲染頁面的過程中,瀏覽器會通過解析HTML文件來構建DOM樹,解析CSS產生CSS規則樹。JavaScript程式碼在解析過程中, 可能會修改生成的DOM樹和CSS規則樹。之後根據DOM樹和CSS規則樹構建渲染樹,在這個過程中CSS會根據選擇器匹配HTML元素。渲染樹包括了每 個元素的大小、邊距等樣式屬性,渲染樹中不包含隱藏元素及head元素等不可見元素。最後瀏覽器根據元素的座標和大小來計算每個元素的位置,並繪製這些元 素到頁面上。重繪指的是頁面的某些部分要重新繪製,比如顏色或背景色的修改,元素的位置和尺寸並沒用改變;迴流則是元素的位置或尺寸發生了改變,瀏覽器需 要重新計算渲染樹,導致渲染樹的一部分或全部發生變化。渲染樹重新建立後,瀏覽器會重新繪製頁面上受影響的元素。迴流的代價比重繪的代價高很多,重繪會影 響部分的元素,而回流則有可能影響全部的元素。如下的這些DOM操作會導致重繪或迴流:
增加、刪除和修改可見DOM元素
頁面初始化的渲染
移動DOM元素
修改CSS樣式,改變DOM元素的尺寸
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" );
});
轉自:https://segmentfault.com/a/1190000009619572推薦↓↓↓
web前端開發熱門新聞,每日 10:24 播報
更多推薦《18個技術類微信公眾號》
涵蓋:程式人生、演算法與資料結構、黑客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。