高頻dom操作和頁面性能優化探索

CesHylton 7年前發布 | 12K 次閱讀 性能優化 前端技術

一、高頻操作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重新構建渲染樹中的相關節點。如果父節點的幾何屬性變化時,還會使其子節點及后續兄弟節點重新計算位置等,造成一系列的重排。
    </li>
  • 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>

          導致頁面重繪的操作

          • 應用新的樣式或者修改任何影響元素外觀的屬性
            • 只改變了元素的樣式,并未改變元素大小、位置,此時只涉及到重繪操作。
            </li>
          • 重排一定會導致重繪
            • 一個元素的重排一定會影響到渲染樹的變化,因此也一定會涉及到頁面的重繪。
            • </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

               

 本文由用戶 CesHylton 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!