優化DOM得從重繪和重排講起,long long ago... 1、重繪和重排 1.1 重繪和重排是什麼 重繪是指一些樣式的修改,元素的位置和大小都沒有改變; 重排是指元素的位置或尺寸發生了變化,瀏覽器需要重新計算渲染樹,而新的渲染樹建立後,瀏覽器會重新繪製受影響的元素。 1.2 瀏覽器渲染頁面 去 ...
優化DOM得從重繪和重排講起,long long ago...
1、重繪和重排
1.1 重繪和重排是什麼
重繪是指一些樣式的修改,元素的位置和大小都沒有改變;
重排是指元素的位置或尺寸發生了變化,瀏覽器需要重新計算渲染樹,而新的渲染樹建立後,瀏覽器會重新繪製受影響的元素。
1.2 瀏覽器渲染頁面
去參加面試總會被問到一個問題,那就是“向瀏覽器輸入一行url會發生什麼?”,這個問題的答案除了要回答網路方面的知識還牽扯到瀏覽器渲染頁面問題。當我們的瀏覽器接收到從伺服器響應的頁面之後便開始逐行渲染,遇到css的時候會非同步的去計算屬性值,再繼續向下解析dom解析完畢之後形成一顆DOM樹,將非同步計算好的樣式(樣式盒子)與DOM樹相結合便成為了一個Render樹,再由瀏覽器繪製在頁面上。DOM樹與Render樹的區別在於:樣式為display:none;的節點會在DOM樹中而不在渲染樹中。瀏覽器繪製了之後便開始解析js文件,根據js來確定是否重繪和重排。
1.3 引起重繪和重排的原因
產生重繪的因素:
- 改變visibility、outline、背景色等樣式屬性,並沒有改變元素大小、位置等。瀏覽器會根據元素的新屬性重新繪製。
產生重排的因素:
- 內容改變
- 文本改變或圖片尺寸改變
- DOM元素的幾何屬性的變化
- 例如改變DOM元素的寬高值時,原渲染樹中的相關節點會失效,瀏覽器會根據變化後的DOM重新排建渲染樹中的相關節點。如果父節點的幾何屬性變化時,還會使其子節點及後續兄弟節點重新計算位置等,造成一系列的重排。
- DOM樹的結構變化
- 添加DOM節點、修改DOM節點位置及刪除某個節點都是對DOM樹的更改,會造成頁面的重排。瀏覽器佈局是從上到下的過程,修改當前元素不會對其前邊已經遍歷過的元素造成影響,但是如果在所有的節點前添加一個新的元素,則後續的所有元素都要進行重排。
- 獲取某些屬性
- 除了渲染樹的直接變化,當獲取一些屬性值時,瀏覽器為取得正確的值也會發生重排,這些屬性包括:
offsetTop
、offsetLeft
、offsetWidth
、offsetHeight
、scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
、clientTop
、clientLeft
、clientWidth
、clientHeight
、getComputedStyle()
。
- 除了渲染樹的直接變化,當獲取一些屬性值時,瀏覽器為取得正確的值也會發生重排,這些屬性包括:
- 瀏覽器視窗尺寸改變
- 視窗尺寸的改變會影響整個網頁內元素的尺寸的改變,即DOM元素的集合屬性變化,因此會造成重排。
- 滾動條的出現(會觸發整個頁面的重排)
總之你要知道,js是單線程的,重繪和重排會阻塞用戶的操作以及影響網頁的性能,當一個頁面發生了多次重繪和重排比如寫一個定時器每500ms改變頁面元素的寬高,那麼這個頁面可能會變得越來越卡頓,我們要儘可能的減少重繪和重排。那麼我們對於DOM的優化也是基於這個開始。
2、優化
2.1 減少訪問
減少訪問次數自然是想到緩存元素,但是要註意
var ele = document.getElementById('ele');
這樣並不是對ele進行緩存,每一次調用ele還是相當於訪問了一次id為ele的節點。
2.1.1 緩存NodeList
var foods = document.getElementsByClassName('food');
我們可以用foods[i]來訪問第i個class為food的元素,不過這裡的foods並不是一個數組,而是一個NodeList。NodeList是一個類數組,保存了一些有序的節點並可以通過位置來訪問這些節點。NodeList對象是動態的,每一次訪問都會運行一次基於文檔的查詢。所以我們要儘量減少訪問NodeList的次數,可以考慮將NodeList的值緩存起來。
// 優化前 var lis = document.getElementsByTagName('li'); for(var i = 0; i < lis.length; i++) { // do something... } // 優化後,將length的值緩存起來就不會每次都去查詢length的值 var lis = document.getElementsByTagName('li'); for(var i = 0, len = lis.length; i < len; i++) { // do something... }
而且由於NodeList是動態變化的,所以如果不緩存可能會引起死迴圈,比如一邊添加元素,一邊獲取NodeList的length。
2.1.2 改變選擇器
獲取元素最常見的有兩種方法,getElementsByXXX()和queryselectorAll(),這兩種選擇器區別是很大的,前者是獲取動態集合,後者是獲取靜態集合,舉個例子。
// 假設一開始有2個li var lis = document.getElementsByTagName('li'); // 動態集合 var ul = document.getElementsByTagName('ul')[0]; for(var i = 0; i < 3; i++) { console.log(lis.length); var newLi = document.createElement('li'); ul.appendChild(newLi); } // 輸出結果:2, 3, 4 var lis = document.querySelector('li'); // 靜態集合 var ul = document.getElementsByTagName('ul')[0]; for(var i = 0; i < 3; i++) { console.log(lis.length); var newLi = document.createElement('li'); ul.appendChild(newLi); } // 輸出結果:2, 2, 2
對靜態集合的操作不會引起對文檔的重新查詢,相比於動態集合更加優化。
2.1.3 避免不必要的迴圈
// 優化前
for(var i = 0; i < 10; i++) {
document.getElementById('ele').innerHTML += 'a';
}
// 優化後
var str = '';
for(var i = 0; i < 10; i++) {
str += 'a';
}
document.getElementById('ele').innerHTML = str;
優化前的代碼訪問了10次ele元素,而優化後的代碼只訪問了一次,大大的提高了效率。
2.1.4 事件委托
js中的事件函數都是對象,如果事件函數過多會占用大量記憶體,而且綁定事件的DOM元素越多會增加訪問dom的次數,對頁面的交互就緒時間也會有延遲。所以誕生了事件委托,事件委托是利用了事件冒泡,只指定一個事件處理程式就可以管理某一類型的所有事件。
// 事件委托前 var lis = document.getElementsByTagName('li'); for(var i = 0; i < lis.length; i++) { lis[i].onclick = function() { console.log(this.innerHTML); }; } // 事件委托後 var ul = document.getElementsByTagName('ul')[0]; ul.onclick = function(event) { console.log(event.target.innerHTML); };
事件委托前我們訪問了lis.length次li,而採用事件委托之後我們只訪問了一次ul。
2.2 減少重繪重排
2.2.1 改變一個dom節點的多個樣式
我們想改變一個div元素的寬度和高度,通常做法可以是這樣
var div = document.getElementById('div1'); div.style.width = '220px';
div.style.height = '300px';
以上操作改變了元素的兩個屬性,訪問了三次dom,觸發兩次重排與兩次重繪。我們說過優化是減少訪問次數以及減少重繪重排次數,從這個出發點可不可以只訪問一次元素以及重排次數降低到1呢?顯然是可以的,我們可以在css里寫一個class
/* css .change { width: 220px; height: 300px; } */ document.getElementById('div').className = 'change';
這樣就達到了一次操作多個樣式
2.2.2 批量修改dom節點樣式
上面代碼的情況是針對於一個dom節點的,如果我們要改變一個dom集合的樣式呢?
第一時間想到的方法是遍歷集合,給每個節點加一個className。再想想這樣豈不是訪問了多次dom節點?想想文章開頭說的dom樹和渲染樹的區別,如果一個節點的display屬性為none那麼這個節點不會存在於render樹中,意味著對這個節點的操作也不會影響render樹進而不會引起重繪和重排,基於這個思路我們可以實現優化:
- 將待修改的集合的父元素display: none;
- 之後遍歷修改集合節點
- 將集合父元素display: block;
// 假設增加的class為.change var lis = document.getElementsByTagName('li'); var ul = document.getElementsByTagName('ul')[0]; ul.style.display = 'none'; for(var i = 0; i < lis.length; i++) { lis[i].className = 'change'; } ul.style.display = 'block';
3、總結
- 減少訪問dom的次數
- 緩存節點屬性值
- 選擇器的使用
- 避免不必要的迴圈
- 事件委托
- 減少重繪與重排
- 使用className改變多個樣式
- 使父元素脫離文檔流再恢復
如果以後看到其他優化方案我會更新,歡迎大家與我交流。
參考文檔: