JavaScript語言層面的優化

JavaScript優化

內存管理

  • 高級語言自帶垃圾回收機制
  • 如果不注意內存管理,可能會導致內存泄漏問題。
  • 內存管理:開發者主動申請空間、使用空間和釋放空間。JavaScript中並未提供相應的API,由執行引擎根據語言來執行內存管理操作。
    • 申請空間let obj = {};
    • 使用空間obj.name = 'foo';
    • 釋放空間obj = null;

垃圾回收與常見GC算法

垃圾回收程序執行時,會阻塞JavaScript的執行。
兩種對垃圾的判斷:

  • 對象不再被引用時,稱爲垃圾
  • 對象不能從根上訪問時,稱爲垃圾

可達對象Reachable

  • 定義:從根開始,可以訪問到的對象就是可達對象(從引用與作用域鏈上可訪問到)
  • 在Javascript裏,根是全局對象(摘自MDN的描述
let obj = { name: 'xm'}; // obj可達對象

let ali = obj; // obj被引用

obj = null; // obj空間被釋放,但ali還引用着
function objGroup(obj1, obj2) {
  obj1.next = obj2;
  obj2.prev = obj1;
  return {
    o1: obj1,
    o2: obj2
  }
}
let obj = objGroup({name: 'obj1'}, {name: 'obj2'});
// 通過下面的兩個delete操作,使得obj1對象不具備可達性,也沒有了引用,就會被當做垃圾回收
delete obj.o1;
delete obj.o2.prev;

GC算法

  • GC :垃圾回收機制
  • GC可以找到內存中的垃圾,釋放和回收空間
  • GC裏的垃圾
    • 程序中不再需要使用的對象
    • 程序中不能再訪問到的對象
  • GC算法就是GC查找回收垃圾時遵循的規則

常見GC算法

引用計數:使用判斷引用的方式
  • 核心思想:爲對象設置引用數,判斷引用數是否爲0,爲0則回收。
  • 引用計數器
  • 引用關係改變時,引用數值會被修改。
優點
  • 發現垃圾立即回收
  • 最大程度減少程序暫停,通過及時釋放空間
缺點
  • 無法回收循環引用對象
  • 時間開銷大(要時刻監控對象的引用計數)
標記清除:使用可達對象的方式

核心思想:標記 + 清除,兩個階段

  • 遍歷所有對象,標記活動對象
  • 遍歷所有對象,清除標記,便於垃圾回收
    回收的空間會放在空閒鏈表中,方便程序申請內存空間。
優點
  • 可以實現對循環引用對象的垃圾回收
缺點
  • 不會立即回收垃圾
  • 空間碎片化,垃圾對象在內存地址上的不連續導致的。
標記整理:標記清除的增強

清除階段不同:先執行整理,移動對象的位置,讓空間可以連續,再清除標記。

分代回收

老生代對象和新生代對象分別採用不同的空間存儲與GC回收算法,見V8引擎。

V8引擎

  • 高效執行JavaScript的引擎
  • 採用即時編譯,源碼編譯爲字節碼,而不是機器碼。字節碼是機器碼的抽象描述,比機器碼更節省空間
  • 內存設限:web應用足夠使用,且如果太大的話,回收程序執行時阻塞時間太長。
    • 64位操作系統時,不超過1.5G
    • 32位操作系統時,不超過800M

V8引擎的垃圾回收

回收主要指的是引用類型,使用分代回收策略

  • 內存分爲新生代、老生代
  • 針對不同對象採用不同算法

V8常用的GC算法有

  • 分代回收
  • 空間複製
  • 標記清除
  • 標記整理
  • 標記增量

V8的內存回收

  • 內存空間一分爲二,左側小空間爲新生代區,右側大空間爲老生代區
  • 小空間存儲新生代對象(32M | 16M)
  • 新生代對象指的是存活時間較短的對象(局部作用域的變量)
新生代對象回收
  • 複製算法 + 標記整理
  • 新生代內存區分爲兩個等大小空間,使用空間爲From,空閒空間爲To,申請內存時使用From空間
  • 活動對象存儲於From空間
  • 標記整理後將活動對象拷貝到To空間,From空間與To空間進行了交換
  • 釋放From空間,這樣就變成了新的To空間
晉升

新生代對象移動到老生代,兩個判斷標準

  • 一輪GC後還存活的新生代對象
  • To空間的使用率超過25%,則將To空間的活動對象放到老生代
老生代對象回收
  • 老生代對象存放在右側老生代區域
  • 64位系統1.4G,32位系統700M
  • 老生代對象是指存活時間較長的對象,如全局下的對象,閉包變量
  • 主要採用標記清除、標記整理、增量標記算法
    • 首先使用標記清除完成垃圾空間回收
    • 採用標記整理算法進行空間優化(當新生代晉升到老生代時,如果空間碎片化導致空間不足是,會執行標記整理)
    • 採用增量標記進行效率優化
增量標記

垃圾回收與JavaScript程序交替執行,避免阻塞JavaScript程序。
標記操作與程序執行交替執行,直到標記完成,然後統一進行清除操作,再進行程序的執行。

Performance工具

意義

對內存使用情況進行監控

使用過程

  • 輸入目標網址(但不要打開)
  • 進入開發者工具,選擇性能
  • 開啓錄製功能,訪問該網址
  • 執行用戶行爲,一段時間後停止錄製
  • 分析界面中記錄的內存信息(需要勾選內存選項)

出現內存問題的體現(在網絡環境正常)

  • 頁面出現延遲加載或經常性暫停,說明GC頻繁進行垃圾回收
  • 頁面持續性出現糟糕性能,說明出現內存膨脹
  • 頁面的性能隨時間越來越差,說明有內存泄漏

監控內存的幾種方式

界定內存問題的標準

  • 內存泄漏:內存使用持續升高
  • 內存膨脹:在多數設備上都存在性能問題
  • 頻繁垃圾回收:通過內存變化圖進行分析
    監控內存的方式
  • 瀏覽器任務管理器
  • TimeLine時序圖記錄
  • 堆快照查找分離DOM
  • 判斷是否存在頻繁的垃圾回收

任務管理器監控內存

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>任務管理器監控內存變化</title>
</head>
<body>
  <button id="btn">add</button>
  <script>
    const oBtn = document.querySelector('#btn');
    oBtn.onclick = function() {
    // 通過點擊事件增加JavaScript使用的內存;
      let arrList = new Array(10000000);
    }
  </script>
</body>
</html>

關注內存一欄與JavaScript內存一欄,這兩欄信息。內存一欄包含了DOM節點使用的內存,而JavaScript內存一欄只是顯示JavaScript使用的內存空間。

TimeLine記錄內存

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Timeline記錄內存變化</title>
</head>
<body>
  <button id="btn">add</button>
  <script>
    const arrList = [];
    // 模擬大內存消耗情況;
    function test() {
      for(let i = 0; i < 100000; i ++) {
        document.body.appendChild(document.createElement('p'));
      }
      // 將數組元素使用x連接成字符串;
      arrList.push(new Array(1000000).join('x'));
    }
    const oBtn = document.querySelector('#btn');
    oBtn.addEventListener('click', test);
    
  </script>
</body>
</html>

堆快照查找分離DOM

開發者工具中的內存面板
什麼是分離DOM

  • 正常DOM節點:界面元素是存活在DOM樹上的DOM節點
  • 垃圾對象的DOM節點:DOM節點從DOM樹剝離,且沒有JavaScript引用
  • 分離狀態detached的DOM節點:DOM節點從DOM樹剝離,但有JavaScript引用,會駐留在內存中。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>堆快照監控內存</title>
</head>
<body>
  <button id="btn">add</button>
  <script>
    let temEle;
    function fn() {
      let ul = document.createElement('ul');
      for(let i = 0; i < 10; i ++) {
        let li = document.createElement('li');
        ul.appendChild(li);
      }
      temEle = ul;
    }

    const oBtn = document.querySelector('#btn');
    // 點擊後,temEle中引用的就是detached DOM;
    oBtn.addEventListener('click', fn);
    
  </script>
</body>
</html>

GC頻繁進行垃圾回收的檢測方法

  • Timeline中頻繁上升下降
  • 任務管理器中數據頻繁的增加減少

JavaScript代碼優化

精準測試JavaScript的性能

  • 本質上是採集大量的執行樣本進行數學統計和分析
  • 使用基於Benchmark.js 的網站來完成

Jsperf使用流程

  • 使用GitHub賬號登錄
  • 填寫個人信息(非必須)
  • 填寫詳細的測試用例信息(title、slug)
  • 填寫準備代碼(DOM操作常用)
  • 填寫必要有setup和teardown代碼
  • 填寫具體的測試代碼片段

慎用全局變量

  • 全局變量定義在全局執行上下文,是所有作用域鏈的頂端,查找時間長
  • 全局變量一直存在於上下文執行棧,直到程序退出
  • 局部作用域出現了同名變量時,會遮蔽全局變量
// 使用全局變量
var i, str = '';
for(i = 0; i < 1000; i ++) {
  str += i;
}
// 使用局部變量
for(let i = 0; i < 1000; i ++) {
  let str = '';
  str += i;
}

在jsperf中檢查兩種方式的性能

緩存全局變量

將使用中無法避免的全局變量緩存到局部。比如在進行DOM查找時,頻繁使用document全局變量,就可以讓document緩存到局部,使得對document的引用查找起來更快。

通過原型新增方法

通過構造函數掛載在實例上的方法和通過在原型上掛載一樣的方法,每一個實例都有一個一樣的方法,顯然是浪費空間的,並且掛載在原型上的方法,調用起來效率更高。

避開閉包陷阱

閉包使用不當容易造成內存泄漏
下面的代碼演示了閉包造成的內存泄漏情況。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>堆快照監控內存</title>
</head>
<body>
  <button id="btn">add</button>
  <script>
    function foo() {
      // 用el變量來引用id爲btn的DOM元素
      // 本來該DOM元素在瀏覽器頁面中就存在引用(可以從root節點找到),我們又使用el引用了該DOM節點
      // 當el引用的DOM元素從DOM樹中刪除時,由於el還存在着該DOM節點的引用,就使得GC無法回收該DOM節點,造成內存泄漏。
      // 因爲我們的操作會經常從DOM樹中刪除節點,一旦操作很多,就會有很多被刪除的DOM節點無法回收內存空間
      var el = document.getElementById('btn');
      el.onclick = function() {
        console.log(el.id);
      }
      // 通過el爲DOM元素掛載了onclick點擊事件處理函數後,將el賦值爲null,可以有效阻止閉包造成的內存泄漏
      // el = null;
    }
    // 執行foo()之後,由於id爲btn的DOM元素掛載了onclick事件處理函數,函數內部對el.id有引用
    // 所以el變量就成了閉包變量,是無法回收的,但實際上el已經沒有什麼用處了。
    foo();
  </script>
</body>
</html>

避免屬性訪問方法的使用

本質上,JavaScript對象的屬性都是外部可見,如果使用方法來控制對屬性的訪問,無疑是增加了一層重定義,沒有訪問的控制力。

for循環優化

  • 對集合的長度用變量進行緩存,不要每輪循環都去獲取。
  • forEach性能最佳、其次是優化後的for循環,最差的是for in 循環

使用字面量而不是構造函數

DOM節點添加的優化

  • 節點的添加操作必定會有迴流與重繪
  • 使用文檔碎片fragmentdocument.createDocumentFragment();來一次性添加很多的節點,比一次次添加這些節點效率要高,因爲迴流與重繪的次數大大減少了

克隆行爲優化創建節點操作

當新增節點時,可以先從已存在的節點中克隆(該節點與我們想要新增的節點有很多相同的屬性等等),比直接新增節點要有效率。

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