在開發 RIA 的過程中,會常常使用到 JavaScript 來變更網頁元素,甚至增加新的網頁元素,而不同的操作方式也會對執行效能有所影響。
瀏覽器在顯示網頁時,會需要計算每一個網頁元素應該放置在哪個位置,這個計算過程就稱為瀏覽器回流(browser reflow)。當我們對 DOM 進行操作(例如更改元素的 CSS 樣式、大小等)或是改變視窗大小時,也會造成瀏覽器的回流,由於瀏覽器的流回需要耗費時間,所以如果可以盡量減少回流,就可以增加整個網頁應用程式的效率。
在操作 DOM 的時候,只會有兩種情況,一種是更動記有的元素,另外一種則是加入新的,以下有四種設計模式(patterns),提供了在這兩種狀況下,減低瀏覽器回流的方式。
CSS 的 Class 變更
這種方式可以讓瀏覽器一次變更多個 CSS 樣式屬性,套用至一個網頁元素以及其所有的子元素(descendants)上,而且只需要耗費一次的瀏覽器回流。
問題
假設我們寫一個 JavaScript 函數,用來更改網頁元素的 CSS 屬性:
function selectAnchor(element) { element.style.fontWeight = 'bold'; element.style.textDecoration = 'none'; element.style.color = '#000'; }
這樣的方式會造成瀏覽器每次更動 DOM 時,都進行一次回流。
解決方案
建立一個包含所有的屬性的 CSS class:
.selectedAnchor { font-weight: bold; text-decoration: none; color: #000; }
然後將這個 CSS class 套用至網頁元素上,這樣就可以讓瀏覽器只耗費一次的回流:
function selectAnchor(element) { element.className = 'selectedAnchor'; }
以 DocumentFragment
處理元素變更
這種方式可以讓我們一次變更多個網頁元素,但是只耗費一次的瀏覽器回流。實作的方式是在 DOM 之外建立一個 DocumentFragment
,將所有要變更的元素都先放在這個 DocumentFragment
之下,處理完之後,再一次將所有的元素移回 DOM 之中,如此一來不管變更多少元素,都只會耗費一次的瀏覽器回流。
問題
下面這個 JavaScript 函數會變更指定元素中所有超連結的 className
屬性,一般我們都會使用迴圈的方式對每一個超連結做變更,不過這樣的方式會造成大量的瀏覽器回流。
function updateAllAnchors(element, anchorClass) { var anchors = element.getElementsByTagName('a'); for (var i = 0, length = anchors.length; i < length; i ++) { anchors[i].className = anchorClass; } }
解決方案
如果要避免產生大量的瀏覽器回流,可以將包含超連結的網頁元素先從 DOM 中移除,等到更改完 className
屬性之後,再將其放回原先的位置。
為了可以達到這樣的目的,我們必須些寫一個小函數,它不只會把元素從 DOM 中移除,還會傳回一個將元素放回 DOM 用的函數。
/** * 從 DOM 移除指定的元素,並且傳回將該元素放回 DOM 用的函數 * @param element {Element} 要暫時移除的元素 * @return {Function} 用來將元素放回原來位置的函數 **/ function removeToInsertLater(element) { var parentNode = element.parentNode; var nextSibling = element.nextSibling; parentNode.removeChild(element); return function() { if (nextSibling) { parentNode.insertBefore(element, nextSibling); } else { parentNode.appendChild(element); } }; }
使用方式就是在變更 className
屬性之前,先以 removeToInsertLater()
將該元素暫時移除,等到變更完成後再將其放回。
function updateAllAnchors(element, anchorClass) { var insertFunction = removeToInsertLater(element); var anchors = element.getElementsByTagName('a'); for (var i = 0, length = anchors.length; i < length; i ++) { anchors[i].className = anchorClass; } insertFunction(); }
建立單一元素
這個設計模式可以讓我們在建立單一網頁元素時,不管建立過程多繁複,都只需要產生一次的瀏覽器回流。
問題
下面這個函數會建立一個新的網頁元素,將該元素加入至 DOM,並設定一些屬性。這樣的狀況會產生三次的瀏覽器回流。
<script> function addAnchor(parentElement, anchorText, anchorClass) { var element = document.createElement('a'); parentElement.appendChild(element); element.innerHTML = anchorText; element.className = anchorClass; }
解決方案
解決方法很簡單,只要將插入 DOM 的動作放在最後,即可將回流次數降低為一次。
function addAnchor(parentElement, anchorText, anchorClass) { var element = document.createElement('a'); element.innerHTML = anchorText; element.className = anchorClass; parentElement.appendChild(element); }
以 DocumentFragment
建立多個元素
這個做法跟上面使用 DocumentFragment
變更元素的方式類似,只不過是用於新增多個元素而已。
問題
這個函數會新增多個超連結元素,並且會產生十次的瀏覽器回流。
function addAnchors(element) { var anchor; for (var i = 0; i < 10; i ++) { anchor = document.createElement('a'); anchor.innerHTML = 'test'; element.appendChild(anchor); } }
解決方案
將所有新增的元素先放在 DocumentFragment
中,最後再一次放進 DOM,這樣就可以只耗費一次瀏覽器回流。
function addAnchors(element) { var anchor, fragment = document.createDocumentFragment(); for (var i = 0; i < 10; i ++) { anchor = document.createElement('a'); anchor.innerHTML = 'test'; fragment.appendChild(anchor); } element.appendChild(fragment); }
參考資料:Google Developers