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

轉自:https://feclub.cn/post/content/domcss


經過js操做DOM的代價很高,影響頁面性能的主要問題有以下幾點:html

  • 訪問和修改DOM元素
  • 修改DOM元素的樣式,致使重繪或重排
  • 經過對DOM元素的事件處理,完成與用戶的交互功能

1、DOM操做影響頁面性能的核心問題

DOM的修改會致使重繪和重排。前端

  • 重繪是指一些樣式的修改,元素的位置和大小都沒有改變;
  • 重排是指元素的位置或尺寸發生了變化,瀏覽器須要從新計算渲染樹,而新的渲染樹創建後,瀏覽器會從新繪製受影響的元素。

頁面重繪的速度要比頁面重排的速度快,在頁面交互中要儘可能避免頁面的重排操做。瀏覽器不會在js執行的時候更新DOM,而是會把這些DOM操做存放在一個隊列中,在js執行完以後按順序一次性執行完畢,所以在js執行過程當中用戶一直在被阻塞。vue


1.頁面渲染過程

一個頁面更新時,渲染過程大體以下:jquery

  • JavaScript: 經過js來製做動畫效果或操做DOM實現交互效果
  • Style: 計算樣式,若是元素的樣式有改變,在這一步從新計算樣式,並匹配到對應的DOM上
  • Layout: 根據上一步的DOM樣式規則,從新進行佈局(重排)
  • Paint: 在多個渲染層上,對新的佈局從新繪製(重繪)
  • Composite: 將繪製好的多個渲染層合併,顯示到屏幕上

在網頁生成的時候,至少會進行一次佈局和渲染,在後面用戶的操做時,不斷的進行重繪或重排,所以若是在js中存在不少DOM操做,就會不斷地出發重繪或重排,影響頁面性能。git


2.DOM操做對頁面性能的影響

如前面所說,DOM操做影響頁面性能的核心問題主要在於DOM操做致使了頁面的重繪或重排,爲了減小因爲重繪和重排對網頁性能的影響,咱們要知道都有哪些操做會致使頁面的重繪或者重排。github


2.1致使頁面重排的一些操做

  • 內容改變
  • 文本改變或圖片尺寸改變
  • DOM元素的幾何屬性的變化
  • 例如改變DOM元素的寬高值時,原渲染樹中的相關節點會失效,瀏覽器會根據變化後的DOM從新排建渲染樹中的相關節點。若是父節點的幾何屬性變化時,還會使其子節點及後續兄弟節點從新計算位置等,形成一系列的重排。
  • DOM樹的結構變化
    • 添加DOM節點、修改DOM節點位置及刪除某個節點都是對DOM樹的更改,會形成頁面的重排。瀏覽器佈局是從上到下的過程,修改當前元素不會對其前邊已經遍歷過的元素形成影響,可是若是在全部的節點前添加一個新的元素,則後續的全部元素都要進行重排。
  • 獲取某些屬性
    • 除了渲染樹的直接變化,當獲取一些屬性值時,瀏覽器爲取得正確的值也會發生重排,這些屬性包括:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、 clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle()。
  • 瀏覽器窗口尺寸改變
    • 窗口尺寸的改變會影響整個網頁內元素的尺寸的改變,即DOM元素的集合屬性變化,所以會形成重排。

2.2致使頁面重繪的操做

  • 應用新的樣式或者修改任何影響元素外觀的屬性
    • 只改變了元素的樣式,並未改變元素大小、位置,此時只涉及到重繪操做。
  • 重排必定會致使重繪
    • 一個元素的重排必定會影響到渲染樹的變化,所以也必定會涉及到頁面的重繪。

2、高頻操做DOM會致使的問題

接下來會分享一下在平時項目中因爲高頻操做DOM影響網頁性能的問題。web

1. 抽獎項目的高頻操做DOM問題

1.1 存在的問題

在最近作的抽獎項目中,就遇到了這樣的因爲高頻操做DOM,致使頁面性能變差的問題。在經歷幾輪抽獎後,文字滾動速度愈來愈慢,肉眼能感覺到與第一次抽獎時文字滾動速度的明顯差異,如持續時間過長或輪次過多,還會形成瀏覽器假死現象。數組

實現demo: https://gxt19940130.github.io/demo/dom.html瀏覽器


1.2 問題分析

下圖爲抽獎時文字滾動過程當中的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操做,形成了頁面的失幀等問題。


1.3 優化方案

針對該項目中的問題,採起的解決方法是:

一次性生成所有

  • ,而且隱藏這些
  • ,隨機生成一組隨機數數組,只有index與數組裏面的隨機數相等時,才顯示該位置的
  • ,雖然也會觸發重排和重繪,可是性能要遠遠高於直接操做DOM的添加和刪除。
    用requestAnimationFrame取代setTimeout不斷生成隨機數。

    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


    1.4 優化先後FPS對比

    優化前文字滾動時的timeline

    優化後文字滾動時的timeline

    優化前的代碼對DOM操做很頻繁,所以FPS值廣泛偏低,而優化後能夠看出紅色方塊明顯減小,FPS值一直處於高值。


    1.5 優化先後CPU佔用對比

    優化前文字滾動時的timeline

    優化後文字滾動時的timeline

    優化前js的CPU佔用率較高,而優化後佔用CPU的主要爲渲染時間,由於優化後的代碼只是控制了節點的顯示和隱藏,因此在js上消耗較少,在渲染上消耗較大。


    2.吸頂導航條相關及scroll滾動優化

    2.1 存在的問題

    吸頂導航條要求當頁面滾動到某個區域時,對應該區域的導航條在設置的顯示範圍內保持吸頂顯示。涉及到的操做:

    • 監聽頁面的scroll事件
    • 在頁面滾動時進行計算和DOM操做
    • 計算:計算當前所在位置是否爲對應導航條的顯示範圍
    • DOM操做:顯示在範圍內的導航條而且隱藏其餘導航條

    因爲scroll事件被觸發的頻率高、間隔近,若是此時進行DOM操做或計算而且這些DOM操做和計算沒法在下一次scroll事件發生前完成,就會形成掉幀、頁面卡頓,影響用戶體驗。


    2.2 優化方案

    針對該項目中的問題,採起的解決方法是:

    • 儘可能控制DOM的顯示或隱藏,而不是刪除或添加:
    • 頁面加載時根據當前頁面中吸頂導航的數量複製對應的DOM,而且隱藏這些導航。當頁面滾動到指定區域後,顯示對應的導航。
    • 一次性操做DOM:
      • 將複製的DOM存儲到數組中,將該數組append到對應的父節點下,而不是根據複製獲得DOM的數量依次循環插入到父節點下。
    • 多作緩存:
      • 若是某個節點將在後續進行屢次操做,能夠將該節點利用變量存儲起來,而不是每次進行操做時都去查找一遍該節點。
    • 使用 requestAnimationFrame優化頁面滾動
    // 在頁面滾動時對顯示範圍進行計算
      // 延遲到整個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 及頁面渲染優化


    3、針對操做DOM的性能優化方法總結

    爲了減小DOM操做對頁面性能產生的影響,在實現頁面的交互效果時必定要注意一下幾點:

    1.減小在循環內進行DOM操做,在循環外部進行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屬性和一次對該屬性從新賦值的操做,所以一樣的循環次數先,優化後的方法執行時間遠遠少於優化前。


    2.只控制DOM節點的顯示或隱藏,而不是直接去改變DOM結構

    在抽獎項目中頻繁操做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,則須要將生成的全部

  • 所有存放在
      下,而且隱藏它們,在根據生成的隨機數組,利用jquery查找index與生成的隨機數對應的
    • 並顯示,達到文字滾動效果。
      優化後demo: https://gxt19940130.github.io/demo/demo_gxt/dom_by_vue.html

      對比結果可查看2.4

      3.操做DOM前,先把DOM節點刪除或隱藏

      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以及其餘的一些操做。

      4. 最小化重繪和重排

      //優化前代碼
      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; 
      })

      到此本文結束,若是對於問題分析存在不正確的地方,還請及時指出,多多交流。

      參考文章:

  • 相關文章
    相關標籤/搜索