在前端開發中,有哪些因素會導致頁面卡頓

前端開發不像後端那樣,很少出現有大量算法的場景,但是前端性能也是需要優化的。好的代碼是保證網頁平穩高性能運行的基礎,結合以往開發中遇到的場景,本文對前端網頁卡頓的原因進行了梳理和分析,並給出了對應的解決方法。

前端頁面卡頓的原因有很多,從渲染機制和運行上可以分爲兩大類,分別是:

  • 渲染不及時,頁面掉幀

  • 網頁內存佔用過高,運行卡頓

兩種類型又可細分如下:

渲染不及時,頁面掉幀

長時間佔用js線程 頁面迴流和重繪較多 資源加載阻塞

內存過大導致的頁面卡頓

內存泄漏導致內存過大

  • 意外的全局變量引起的內存泄漏

  • 閉包引起的內存泄漏

  • 被遺忘的定時器

  • 循環引用

  • DOM刪除時沒有解綁事件

  • 沒有清理的DOM元素引用

dom節點或事件佔用內存過大

一、渲染

1,長時間佔用js線程

瀏覽器包括js線程和GUI線程,而二者是互斥的,當長時間佔用js線程時,會導致渲染不及時,出現頁面卡頓。

實例1:

document.body.html('爲什麼不先渲染我');

//程序
$.ajax({
   url: '',
   async: false
})


//運行結果會在ajax執行完畢後,再去渲染頁面

採用同步方式獲取數據,會導致gui線程掛起,數據返回後再執行渲染。

實例2:

function a (){
    // arr的長度過長時會導致頁面卡頓
    arr.forEach(item => {
        ...
    })
}

let inputDom = document.getElementById('input')
let arr = document.getElementsByClassName('.tag')
input.onchange = a

計算時間過長導致頁面渲染不及時

渲染不及時的原因:

瀏覽器的渲染頻率一般是60HZ,即要求1幀的時間爲1s / 60 = 16.67ms,瀏覽器顯示頁面的時候,要處理js邏輯,還要做渲染,每個執行片段不能超過16.67ms。實際上,瀏覽器內核自身支撐體系運行也需要消耗一些時間,所以留給我們的時間差不多隻有10ms。

常見的優化方式:

  • 採用requestIdleCallback和requestAnimationFrame,任務分片

實例

function Task(){
    this.tasks = [];
}
//添加一個任務
Task.prototype.addTask = function(task){
    this.tasks.push(task);
};
//每次重繪前取一個task執行
Task.prototype.draw = function(){
    var that = this;
    window.requestAnimationFrame(function(){
        var tasks = that.tasks;        if(tasks.length){
            var task = tasks.shift();
            task();
        }
        window.requestAnimationFrame(function(){that.draw.call(that)});
    });
};

2,頁面迴流和重繪較多

  • 儘量減少layout

獲取scrollTop、clentWidth等維度屬性時都會觸發layout以獲取實時的值,所以在for循環裏面應該把這些值緩存一下

實例:

優化之前

for(var i = 0; i < childs.length; i++){
   childs.style.width = node.offsetWidth + "px";
}

優化之後

var width = node.offsetWidth;for(var i = 0; i < childs.length; i++){
   childs.style.width = width + "px";
}
  • 簡化DOM結構

當DOM結構越複雜時,需要重繪的元素也就越多。所以dom應該保持簡單,特別是那些要做動畫的,或者要監聽scroll/mousemove事件的。另外使用flex比使用float在重繪方面會有優勢。

3,資源加載阻塞

  • js資源放在body之前

  • 行內script阻塞

  • css加載會阻塞DOM樹渲染(css並不會阻塞DOM樹的解析)

  • 資源過大阻塞

二、內存過大導致的頁面卡頓

1,內存泄漏導致內存過大

瀏覽器有自己的一套垃圾回收機制,主流垃圾回收機制是標記清除,不過在ie中訪問原生dom會採用引用計數方式機制,而如果閒置內存得不到及時回收,就會導致內存泄漏。

簡單介紹下兩種垃圾回收機制(GC Garbage Collection)

標記清除:

定義和用法:

當變量進入環境時,將變量標記"進入環境",當變量離開環境時,標記爲:"離開環境"。某一個時刻,垃圾回收器會過濾掉環境中的變量,以及被環境變量引用的變量,剩下的就是被視爲準備回收的變量。

到目前爲止,IE、Firefox、Opera、Chrome、Safari的js實現使用的都是標記清除的垃圾回收策略或類似的策略,只不過垃圾收集的時間間隔互不相同。

流程:

  • 瀏覽器在運行的時候會給存儲再內存中的所有變量都加上標記

  • 去掉環境中的變量以及被環境中引用的變量的標記

  • 如果還有變量有標記,就會被視爲準備刪除的變量

  • 垃圾回收機制完成內存的清除工作,銷燬那些帶標記的變量,並回收他們所佔用的內存空間

引用計數

定義和用法:引用計數是跟蹤記錄每個值被引用的次數。

基本原理:就是變量的引用次數,被引用一次則加1,當這個引用計數爲0時,被視爲準備回收

的對象。

流程

  • 聲明瞭一個變量並將一個引用類型的值賦值給這個變量,這個引用類型值引用次數就是1

  • 同一個值又被賦值另一個變量,這個引用類型的值引用次數加1

  • 當包含這個引用類型值得變量又被賦值另一個值了,那麼這個引用類型的值的引用次數減1

  • 當引用次數變成0時, 說明這個值需要解除引用

  • 當垃圾回收機制下次運行時,它就會釋放引用次數爲0 的值所佔用的內存

常見的造成內存泄漏的原因:

  • 意外的全局變量引起的內存泄漏

解決:使用嚴格模式避免。

實例

<button onclick="createNode()">添加節點</button>
<button onclick="removeNode()">刪除節點</button>
<div id="wrapper"></div>
<script>
  var text = [];
  function createNode() { 
      text.push(new Array(1000000).join('x'));  
      var textNode = document.createTextNode("新節點"),
          div = document.createElement('div');
      div.appendChild(textNode);
      document.getElementById("wrapper").appendChild(div);  
  }
  
  function removeNode() {
      var wrapper = document.getElementById("wrapper"),
          len = wrapper.childNodes.length;
      if (len > 0) {
          wrapper.removeChild(wrapper.childNodes[len - 1]);  
      }
  }
  </script>

text變量在createNode中引用,導致text不能被回收

  • 閉包引起的內存泄漏

實例:

<button onclick="replaceThing()">第二次點我就有泄漏</button>
  <script>
  var theThing = null;
  var replaceThing = function () {
      var originalThing = theThing;
      var unused = function () {
          if (originalThing) {
              console.log("hi");
          };
      }
      theThing = {
          longStr: new Array(1000000).join('*'),
          someMethod: function someMethod() {
              console.log('someMessage');
          }
      };
  };

上面那段代碼泄漏的原因在於有兩個閉包:unused和someMethod,二者共享父級作用域。

因爲後面的 theThing 是全局變量,someMethod是全局變量的屬性,它引用的閉包作用域(unused 和somMethod共享)不會釋放,由於originalThing在共享的作用域中,造成originalThing不會釋放,隨着 replaceThing 不斷調用,originalThing 指向前一次的 theThing,而新的theThing.someMethod又會引用originalThing ,從而形成一個閉包引用鏈,而 longStr是一個大字符串,得不到釋放,從而造成內存泄漏。

解決方法:在 replaceThing 的最後添加 originalThing = null

  • 被遺忘的定時器

實例

var someResource = getData(); 
setInterval(function() { 
    var node = document.getElementById('Node'); 
    if(node) { 
        // 處理 node 和 someResource 
        node.innerHTML = JSON.stringify(someResource)); 
    } 
}, 1000);

計時器回調函數沒被回收(計時器停止纔會被回收)

  • 循環引用

循環引用就是對象A中包含另一個指向對象B的指針,B中也包含一個指向A的引用。

因爲IE中的BOM、DOM的實現使用了COM,而COM對象使用的垃圾收集機制是引用計數策略。所以會存在循環引用的問題

解決方法:手工斷開js對象和DOM之間的鏈接。賦值爲null。

實例:

function handle () {
    var element = document.getElementById(“testId”);
    element.onclick = function (){
        alert(element.id)
    }
}

element綁定的事件中引用了element上的屬性

onclick事件是一個閉包,閉包可以維持函數內局部變量,使其得不到釋放。也就是說element變量得不到釋放,每調用一次element都會得不到釋放,最終內存泄漏

解決方法:

function handle () {
    var element = document.getElementById(“testId”);
    element.onclick = function (){
        alert(element.id)
    }
    element = null
}
  • DOM刪除時沒有解綁事件

比如刪除一個button,但是並沒有解除button上的事件

  • 沒有清理的DOM元素引用

2,dom節點或事件佔用內存過大

詳細分析見我另外一篇文章 網頁dom元素過多爲什麼會導致頁面卡頓

實例:

function addDom(){
        let d = document.createDocumentFragment();
       
        for(var i = 0;i<30;i++){
            let li = document.createElement('li')
            li.addEventListener('click', function(e) {
                let _this = e.target;
                let dom = e.target.tagName.toLowerCase();
                _this.style.color = 'red';
            })
        li.innerHTML = `</div>
            <h4>
                測試圖片 
            </h4>
            <img style = "height:20px;width:100px" src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1591105123139&di=90a63b4962d0b4405ce65c2096a676c2&imgtype=0&src=http%3A%2F%2Fimg0.imgtn.bdimg.com%2Fit%2Fu%3D3769023270%2C3433174748%26fm%3D214%26gp%3D0.jpg"/>
            </div>`
            d.appendChild(li)
        }
        document.getElementById('app').appendChild(d)
    }

上面的代碼是下拉加載,每次都會添加dom,最終導致內存過大

解決辦法:採用虛擬列表和事件委託

總結:頁面卡頓在實際開發過程中有很多場景,可以使用內存泄漏檢測工具(sIEve,針對IE)進行檢測,也可以使用chrome提供的timeline和profiles,或者performance,這裏不再詳細介紹。

參考:https://www.cnblogs.com/yanglongbo/articles/9762359.html

https://blog.csdn.net/c11073138/article/details/84728132

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章