Javascript記憶體洩漏

老毛發表於2013-04-05
在傳統的網頁開發時無需過多考慮記憶體管理,通常也不會產生嚴重的後果。因為當使用者點選連結開啟新頁面或者重新整理頁面,頁面內的資訊就會從記憶體中清理掉。
隨著SPA(Single Page Application)應用的增多,迫使我們在編碼時需要更多的關注記憶體。因為如果應用使用的記憶體逐漸增多會直接影響到網頁的效能,甚至導致瀏覽器標籤頁崩潰。
這篇文章,我們將研究JavaScript編碼導致記憶體洩漏的場景,提供一些記憶體管理的建議。

什麼是記憶體洩漏?

我們知道瀏覽器會把object儲存在堆記憶體中,它們通過索引鏈可以被訪問到。GC(Garbage Collector) 是一個JavaScript引擎的後臺程式,它可以鑑別哪些物件是已經處於無用的狀態,移除它們,釋放佔用的記憶體。

本該被GC回收的變數,如果被其他物件索引,而且可以通過root訪問到,這就意味著記憶體中存在了冗餘的記憶體佔用,會導致應用的效能降級,這時也就發生了記憶體洩漏。

怎樣發現記憶體洩漏?

記憶體洩漏一般不易察覺和定位,藉助瀏覽器的內建工具可以幫助我們分析是否存在記憶體洩漏,和導致記憶體洩漏的原因。

開發者工具

開啟開發者工具-Performance選項卡,可以分析當前頁面的視覺化資料。Chrome 和 Firefox 都有出色的記憶體分析工具,通過分析快照為開發者提供記憶體的分配情況。

JS導致記憶體洩漏的常見情形

未被注意的全域性變數

全域性變數可以被root訪問,不會被GC回收。一些非嚴格模式下的區域性變數可能會變成全域性變數,導致記憶體洩漏。

  • 給沒有宣告的變數賦值
  • this 指向全域性物件
function createGlobalVariables() {
  leaking1 = 'I leak into the global scope'; // assigning value to the undeclared variable
  this.leaking2 = 'I also leak into the global scope'; // 'this' points to the global object
};
createGlobalVariables();
window.leaking1; // 'I leak into the global scope'
window.leaking2; // 'I also leak into the global scope'

如何避免?使用嚴格模式。

閉包

閉包函式執行完成後,作用域中的變數不會被回收,可能會導致記憶體洩漏:

function outer() {
  const potentiallyHugeArray = [];

  return function inner() {
    potentiallyHugeArray.push('Hello'); // function inner is closed over the potentiallyHugeArray variable
    console.log('Hello');
  };
};
const sayHello = outer(); // contains definition of the function inner

function repeat(fn, num) {
  for (let i = 0; i < num; i++){
    fn();
  }
}
repeat(sayHello, 10); // each sayHello call pushes another 'Hello' to the potentiallyHugeArray 
 
// now imagine repeat(sayHello, 100000)

定時器

使用setTimeout 或者 setInterval:

function setCallback() {
  const data = {
    counter: 0,
    hugeString: new Array(100000).join('x')
  };

  return function cb() {
    data.counter++; // data object is now part of the callback's scope
    console.log(data.counter);
  }
}

setInterval(setCallback(), 1000); // how do we stop it?

只有當定時器被清理掉的時候,它回撥函式內部的data才會被從記憶體中清理,否則在應用退出前一直會被保留。

如何避免?

function setCallback() {
  // 'unpacking' the data object
  let counter = 0;
  const hugeString = new Array(100000).join('x'); // gets removed when the setCallback returns
  
  return function cb() {
    counter++; // only counter is part of the callback's scope
    console.log(counter);
  }
}

const timerId = setInterval(setCallback(), 1000); // saving the interval ID

// doing something ...

clearInterval(timerId); // stopping the timer i.e. if button pressed

定時器賦值給timerId,使用clearInterval(timerId)手動清理。

Event listeners

addEventListener 也會一直保留在記憶體中無法回收,直到我們使用了 removeEventListener,或者新增監聽事件的DOM被移除。

const hugeString = new Array(100000).join('x');

document.addEventListener('keyup', function() { // anonymous inline function - can't remove it
  doSomething(hugeString); // hugeString is now forever kept in the callback's scope
});

如何避免?

function listener() {
  doSomething(hugeString);
}

document.addEventListener('keyup', listener); // named function can be referenced here...
document.removeEventListener('keyup', listener); // ...and here

// 或者
document.addEventListener('keyup', function listener() {
  doSomething(hugeString);
}, {once: true}); // listener will be removed after running once

JS記憶體洩漏總結

鑑別和修復JS記憶體使用問題是一項有挑戰性的任務,編碼過程中也要把避免記憶體洩漏放在第一位。因為這關係到了應用的效能和使用者體驗。

本文翻譯自 《Causes of Memory Leaks in JavaScript and How to Avoid Them》

相關文章