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節點添加的優化
- 節點的添加操作必定會有迴流與重繪
- 使用文檔碎片fragment
document.createDocumentFragment();
來一次性添加很多的節點,比一次次添加這些節點效率要高,因爲迴流與重繪的次數大大減少了
克隆行爲優化創建節點操作
當新增節點時,可以先從已存在的節點中克隆(該節點與我們想要新增的節點有很多相同的屬性等等),比直接新增節點要有效率。