轉自:https://feclub.cn/post/content/domcss
經過js操做DOM的代價很高,影響頁面性能的主要問題有以下幾點:html
DOM的修改會致使重繪和重排。前端
頁面重繪的速度要比頁面重排的速度快,在頁面交互中要儘可能避免頁面的重排操做。瀏覽器不會在js執行的時候更新DOM,而是會把這些DOM操做存放在一個隊列中,在js執行完以後按順序一次性執行完畢,所以在js執行過程當中用戶一直在被阻塞。vue
一個頁面更新時,渲染過程大體以下:jquery
在網頁生成的時候,至少會進行一次佈局和渲染,在後面用戶的操做時,不斷的進行重繪或重排,所以若是在js中存在不少DOM操做,就會不斷地出發重繪或重排,影響頁面性能。git
如前面所說,DOM操做影響頁面性能的核心問題主要在於DOM操做致使了頁面的重繪或重排,爲了減小因爲重繪和重排對網頁性能的影響,咱們要知道都有哪些操做會致使頁面的重繪或者重排。github
接下來會分享一下在平時項目中因爲高頻操做DOM影響網頁性能的問題。web
在最近作的抽獎項目中,就遇到了這樣的因爲高頻操做DOM,致使頁面性能變差的問題。在經歷幾輪抽獎後,文字滾動速度愈來愈慢,肉眼能感覺到與第一次抽獎時文字滾動速度的明顯差異,如持續時間過長或輪次過多,還會形成瀏覽器假死現象。數組
實現demo: https://gxt19940130.github.io/demo/dom.html瀏覽器
下圖爲抽獎時文字滾動過程當中的timeline記錄。
timeline分析:
- FPS:最上面一欄爲綠色柱形爲幀率(FPS),頂點值爲60fps,上方紅色方塊表示長幀,這些長幀被Chrome稱爲jank(卡頓)。
- CPU:第二欄爲CPU,藍色表示loading(網絡通訊和HTML解析),黃色表示scripting(js執行時間),紫色表示rendering(樣式計算和佈局,即重排), 綠色爲painting(即重繪)。
更多timeline使用方法可參考:如何使用Chrome Timeline 工具(譯)
由上圖能夠看出,在文字滾動過程當中紅色方塊出現頻繁,頁面中存在的卡頓過多。幀率的值越低,人眼感覺到的效果越差。
參考文章:腦洞大開:爲啥幀率達到 60 fps 就流暢?
接下來選擇一段長幀區域放大來看
在這段區域內最大一幀達到了49.7ms,幀率只有20fps,接下來看看這一幀裏是什麼因素耗時過長
由上圖能夠看出,耗時最大的在scripting,js的執行時間達到了44.9ms,佔總時間的93.2%,由於主要靠js計算控制DOM的顯示內容,因此js運行時間過長。
選取一段FPS值很低的部分查看形成這段值低的緣由
由下圖可看出主要爲dom.html中的js執行佔用時間。
點進dom.html文件,便可定位到該函數
由此可知,主要是rolling這個函數執行時間過長,對該部分失幀影響較大。而這個函數的主要做用就是實現文字的滾動效果,也能夠從代碼中看出,這個函數利用的setTimeout來反覆執行,而且在這個函數中存在着循環以及大量的DOM操做,形成了頁面的失幀等問題。
針對該項目中的問題,採起的解決方法是:
一次性生成所有
requestAnimationFrame與setTimeout和setInterval相似,都是經過遞歸調用同一個方法不斷更新頁面。
- setTimeout():在特定的時間後執行函數,並且只執行一次,若是在特定時間前想取消執行函數,能夠用clearTimeout當即取消執行。可是並非每次執行setTimeout都會在特定的時間後執行,頁面加載後js會按照主線程中的順序按序執行那個,若是在延遲時間內主線程不空閒,setTimeout裏面的函數是不會執行的,它會延遲到主線程空閒時才執行。
- setInterval():在特定的時間間隔內重複執行函數,除非主動清除它,否則會一直執行下去,清除函數可使用clearInterval。setInterval也會等到主線程空閒了再執行,可是setInterval去排隊時,若是發現本身還在隊列中未執行,就會被drop掉,因此可能會形成某段時間的函數未被執行。
- requestAnimationFrame():它不須要設置時間間隔,它會在瀏覽器每次刷新以前執行回調函數的任務。這樣咱們動畫的更新就能和瀏覽器的刷新頻率保持一致。requestAnimationFrame在運行時,瀏覽器會自動優化方法的調用,而且若是頁面不是激活狀態下的話,動畫會自動暫停,有效節省了CPU開銷。
在採用上面的方法進行優化後,在經歷多輪抽獎後,文字滾動速度依舊正常,網頁性能良好,不會出現文字滾動速度愈來愈慢,最後致使瀏覽器假死的現象。
實現demo: https://gxt19940130.github.io/demo/demo_gxt/dom_by_vue.html
優化前文字滾動時的timeline
優化後文字滾動時的timeline
優化前的代碼對DOM操做很頻繁,所以FPS值廣泛偏低,而優化後能夠看出紅色方塊明顯減小,FPS值一直處於高值。
優化前文字滾動時的timeline
優化後文字滾動時的timeline
優化前js的CPU佔用率較高,而優化後佔用CPU的主要爲渲染時間,由於優化後的代碼只是控制了節點的顯示和隱藏,因此在js上消耗較少,在渲染上消耗較大。
吸頂導航條要求當頁面滾動到某個區域時,對應該區域的導航條在設置的顯示範圍內保持吸頂顯示。涉及到的操做:
因爲scroll事件被觸發的頻率高、間隔近,若是此時進行DOM操做或計算而且這些DOM操做和計算沒法在下一次scroll事件發生前完成,就會形成掉幀、頁面卡頓,影響用戶體驗。
針對該項目中的問題,採起的解決方法是:
// 在頁面滾動時對顯示範圍進行計算 // 延遲到整個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的滾動優化還能夠採用防抖(Debouncing)和節流(Throttling)的方式,可是防抖和節流的方式仍是要藉助於setTimeout,所以和requestAnimationFrame相比,仍是requestAnimationFrame實現效果好一些。
參考文章:高性能滾動 scroll 及頁面渲染優化
爲了減小DOM操做對頁面性能產生的影響,在實現頁面的交互效果時必定要注意一下幾點:
//優化前代碼 function Loop() { console.time("loop1"); for (var count = 0; count < 15000; count++) { document.getElementById('text').innerHTML += 'dom'; } console.timeEnd("loop1"); }
//優化後代碼 function Loop2() { console.time("loop2"); var content = ''; for (var count = 0; count < 15000; count++) { content += 'dom'; } document.getElementById('text2').innerHTML += content; console.timeEnd("loop2"); }
兩個函數的執行時間對比:
優化前的代碼中,每進行一次循環,都會讀取一次div的innerHtml屬性,而且對這個屬性進行了從新賦值,即每循環一次就會操做兩次DOM,所以執行時間很長,頁面性能差。
在優化後的代碼中,將要更新的DOM內容進行緩存,在循環時只操做字符串,循環結束後字符串的值寫入到div中,只進行了一次查找innerHtml屬性和一次對該屬性從新賦值的操做,所以一樣的循環次數先,優化後的方法執行時間遠遠少於優化前。
在抽獎項目中頻繁操做DOM來控制文字滾動的方法(demo:https://gxt19940130.github.io/demo/dom.html 致使頁面性能不好,最後修改成以下代碼。
<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,根據計算的隨機數來控制顯示某個
若是採用jquery,則須要將生成的全部
對比結果可查看2.4
var list1 = $(".list1"); list1.hide(); for (var i = 0; i < 15000; i++) { var item = document.createElement("li"); item.append(document.createTextNode('0')); list1.append(item); } list1.show();
display屬性值爲none的元素不在渲染樹中,所以對隱藏的元素操做不會引起其餘元素的重排。若是要對一個元素進行屢次DOM操做,能夠先將其隱藏,操做完成後再顯示。這樣只在隱藏和顯示時觸發2次重排,而不會是在每次進行操做時都出發一次重排。
頁面rendering時間對比:
下圖爲一樣的循環次數下未隱藏節點直接進行DOM操做的rendering時間(圖一)和隱藏節點再進行DOM操做的rendering時間(圖二)
由對比圖能夠看出,總時間、js執行時間以及rendering時間都明顯減小,而且避免了painting以及其餘的一些操做。
//優化前代碼 var element = document.getElementById('mydiv'); element.style.height = "100px"; element.style.borderLeft = "1px"; element.style.padding = "20px";
在上面的代碼中,每對element進行一次樣式更改都會影響該元素的集合結構,最糟糕狀況下會觸發三次重排。
優化方式:利用js或jquery對該元素的class從新賦值,得到新的樣式,這樣減小了屢次的DOM操做。
//優化後代碼 //js操做 .newStyle { height: 100px; border-left: 1px; padding: 20px; } element.className = "newStyle"; //jquery操做 $(element).css({ height: 100px; border-left: 1px; padding: 20px; })
到此本文結束,若是對於問題分析存在不正確的地方,還請及時指出,多多交流。
參考文章: