高頻dom操作和頁面性能優化探索
一、高頻操作DOM會導致的問題
DOM的修改會導致 重繪 和 重構 ,重繪意味著網頁樣式的改變比如背景顏色、字體顏色等,重構意味著結構的改變,消耗性能要大于重繪,瀏覽器不會在js執行的時候更新dom,而是會把這些dom操作存放在一個隊列中,在js執行完之后按順序一次性執行完畢,因此在js執行過程中用戶一直在被阻塞。
1.年會抽獎項目的高頻操作DOM問題
在最近做的年會抽獎項目中,就遇到了這樣的高頻操作DOM,嚴重影響頁面性能的問題,在經歷幾輪抽獎后,文字滾動速度越來越慢,肉眼能感受到與第一次抽獎時文字滾動速度的明顯差別,如持續時間過長或輪次過多,還會造成瀏覽器假死現象。
實現demo: https://gxt19940130.github.io/demo/dom.html
衡量頁面性能一個重要的指標是fps,即幀率(每秒幀數),幀率越高,頁面運行越流暢。
由下圖demo的timeline可以看出,fps顯示為紅色的占多數,這個demo中的幀率多數在20~45fps之間,頁面會出現嚴重的掉幀的情況,當幀率低于24fps時,肉眼就會感覺到頁面存在卡頓現象,所以用這種頻繁操作DOM來實現文字滾動效果的方法寫出的頁面性能很差。
針對該項目中的問題,采取的解決方法是:
- 一次性生成全部 <li> ,并且隱藏這些 <li> ,隨機生成一組隨機數數組,只有index與數組里面的隨機數相等時,才顯示該位置的 <li> 。
- 用 requestAnimationFrame 取代 setTimeout 不斷生成隨機數。
requestAnimationFrame與setTimeout和setInterval類似,都是通過遞歸調用同一個方法不斷更新頁面。但是setTimeout和setInterval都存在性能上的問題,而requestAnimationFrame在運行時,瀏覽器會自動優化方法的調用,并且如果頁面不是激活狀態下的話,動畫會自動暫停,有效節省了CPU開銷。
在采用上面的方法進行優化后,在經歷多輪抽獎后,文字滾動速度依舊正常,網頁性能良好,不會出現文字滾動速度越來越慢,最后導致瀏覽器假死的現象。
2.頂部導航條相關及scroll滾動優化
頂部導航條要求當頁面滾動到某個區域時,對應該區域的導航條在設置的顯示范圍內吸頂顯示,因此需要監聽頁面的scroll事件,并在頁面滾動時進行計算和DOM操作。
// 在頁面滾動時對顯示范圍進行計算
// 延遲到整個dom加載完后再調用,并且異步到所有事件后執行
$(function(){
//animationShow優化滾動效果,scrollShow為實際計算顯示范圍及操作DOM的函數
setTimeout( function() {
window.Scroller.on('scrollend', animationShow);
window.Scroller.on('scrollmove', animationShow);
})
});
function animationShow(){
return window.requestAnimationFrame ?window.requestAnimationFrame(scrollShow) : scrollShow();
}
scroll事件被觸發的頻率高、間隔近,如果此時進行DOM操作或計算并且這些DOM操作和計算無法在下一次scroll事件發生前完成,就會造成掉幀、頁面卡頓,影響用戶體驗。
針對該項目中的問題,采取的解決方法是:
- 盡量控制DOM的顯示或隱藏,而不是刪除或添加。頁面加載時根據當前頁面中吸頂導航的數量復制對應的DOM,并且隱藏這些導航。當頁面滾動到指定區域后,顯示對應的導航。
- 一次性操作DOM,將復制的DOM存儲到數組中,將該數組append到對應的父節點下,而不是根據復制得到DOM的數量依次循環插入到父節點下。
- 多做緩存,如果某個節點將在后續進行多次操作,可以將該節點利用變量存儲起來,而不是每次進行操作時都去查找一遍該節點。
二、DOM操作影響頁面性能的核心問題
頁面加載時,瀏覽器會根據HTML構建DOM樹,再根據CSS和DOM樹構建渲染樹。如前面所說, DOM操作影響頁面性能的核心問題主要是頁面的重繪和重排 。
- 重繪是指一些樣式的修改,元素的位置和大小都沒有改變;
- 重排是指元素的位置或尺寸發生了變化,瀏覽器需要重新計算渲染樹,而新的渲染樹建立后,瀏覽器會重新繪制受影響的元素。因此頁面重繪的速度要比頁面重排的速度快,在頁面交互中要盡量避免頁面的重排操作。
導致頁面重排的一些操作:
- DOM元素的幾何屬性的變化
- 例如改變DOM元素的寬高值時,原渲染樹中的相關節點會失效,瀏覽器會根據變化后的DOM重新構建渲染樹中的相關節點。如果父節點的幾何屬性變化時,還會使其子節點及后續兄弟節點重新計算位置等,造成一系列的重排。
- DOM樹的結構變化
- 添加DOM節點、修改DOM節點位置及刪除某個節點都是對DOM樹的更改,會造成頁面的重排。瀏覽器布局是從上到下的過程,修改當前元素不會對其前邊已經遍歷過的元素造成影響,但是如果在所有的節點前添加一個新的元素,則后續的所有元素都要進行重排。 </ul> </li>
- 獲取某些屬性
- 除了渲染樹的直接變化,當獲取一些屬性值時,瀏覽器為取得正確的值也會發生重排,這些屬性包括: offsetTop 、 offsetLeft 、 offsetWidth 、 offsetHeight 、 scrollTop 、 scrollLeft 、 scrollWidth 、 scrollHeight 、 clientTop 、 clientLeft 、 clientWidth 、 clientHeight 、 getComputedStyle() 。 </ul> </li>
- 瀏覽器窗口尺寸改變
- 窗口尺寸的改變會影響整個網頁內元素的尺寸的改變,即DOM元素的集合屬性變化,因此會造成重排。 </ul> </li> </ul>
- 應用新的樣式或者修改任何影響元素外觀的屬性
- 只改變了元素的樣式,并未改變元素大小、位置,此時只涉及到重繪操作。
- 重排一定會導致重繪
- 一個元素的重排一定會影響到渲染樹的變化,因此也一定會涉及到頁面的重繪。 </ul> </li> </ul>
三、針對操作DOM的性能優化方法
1.減少在循環內進行DOM操作,在循環外部進行DOM緩存
//優化前代碼 var _li = $("<li>"), _dom = $("<div>"), timer = null; for (var i = 0; i < 50; i++) { //隨機生成50個li,插入到ul列表中 $(".list-ul").append(_li.clone()); }
//優化后代碼 var _li = $("<li>"), _dom = $("<div>"), _lis = document.getElementsByTagName("li"), timer = null, _arr = []; for (var i = 0; i < 50; i++) { //隨機生成50個li,存入到數組中 _arr.push(_li.clone()); } //將生成好的全部li一次性append到ul中 $(".list-ul").append(_arr);
優化前的代碼中,對于 $(".list-ul") 元素進行了50次的append,即進行了50次的DOM操作。而對于優化后的代碼,在append操作前,先將所有 <li> 存入數組中,最后只進行了一次append,因此性能會有所提高。
2.只控制DOM節點的顯示或隱藏,而不是直接去改變DOM結構
在年會抽獎項目中頻繁操作DOM來控制文字滾動的方法( demo ),導致頁面性能很差,最后修改為如下代碼。
<div class="staff-list" :class="list"> <ul class="staff-list-ul"> <li v-for="item in staffList" v-show="isShow($index)"> <div>{{{item.staff_name | addSpace}}} </div> <div class="staff_phone">{{item.phone_no}} </div> </li> </ul> </div>
上面代碼的優化原理即先生成所有DOM節點,但是所有節點均不顯示出來,利用vue.js中的 v-show ,根據計算的隨機數來控制顯示某個 <li> ,來達到文字滾動效果。
如果采用jquery,則需要將生成的所有 <li> 全部存放在 <ul> 下,并且隱藏它們,在根據生成的隨機數組,利用jquery查找index與生成的隨機數對應的 <li> 并顯示,達到文字滾動效果。
3.操作DOM前,先把DOM節點刪除或隱藏
list.style.display = "none"; for (var i=0; i < items.length; i++){ var item = document.createElement("li"); item.appendChild(document.createTextNode("Option " + i); list.appendChild(item); } list.style.display = "";
display屬性值為none的元素不在渲染樹中,因此對隱藏的元素操作不會引發其他元素的重排。如果要對一個元素進行多次DOM操作,可以先將其隱藏,操作完成后再顯示。這樣只在隱藏和顯示時觸發2次重排,而不會是在每次進行操作時都出發一次重排。
4.一次性修改樣式和屬性,不要每次只修改一個
//優化前代碼 element.style.backgroundColor = "blue"; element.style.color = "red"; element.style.fontSize = "20px";
//優化后代碼 //js操作 .newStyle { background-color: blue; color: red; font-size: 20px; } element.className = "newStyle"; //jquery操作 $(element).css({ background-color: blue; color: red; font-size: 20px; })
優化前的代碼每一次更改樣式都會查找一次該元素進行一次DOM操作,而優化后的代碼,對于要修改的幾個樣式,都是只進行一次查找操作,因此只進行了一次DOM操作,避免了多次重繪或者重排。
來自:http://feclub.cn/post/content/dom
導致頁面重繪的操作