JavaScript 工做原理之三-內存管理及如何處理 4 類常見的內存泄漏問題(譯)

原文請查閱這裏javascript

本系列持續更新中,Github 地址請查閱這裏html

這是 JavaScript 工做原理的第三章。前端

咱們將會討論平常使用中另外一個被開發者愈來愈忽略的重要話題,這都是日益成熟和複雜的編程語言的鍋,即內存管理問題。咱們將會提供在建立 SessionStack 的時候所遵循的處理 JavaScript 內存泄漏的幾條小技巧,由於咱們須要保證 SessionStack 不會引發內存泄漏或者不會增長咱們所集成的 web 應用程序的內存消耗。java

概述

像 C 語言擁有底層的內存管理原語如 malloc()free()。開發者使用這些原語來顯式從操做系統分配和釋放內存。git

與此同時,當建立事物(對象,字符串等)的時候,JavaScript 分配內存而且當它們再也不使用的時候 "自動釋放" 內存,這一過程稱爲內存垃圾回收。這個乍看起來本質上是 "自動化釋放內存" 的釋放資源是引發混亂的緣由,而且給予 JavaScript(及其它高級語言)開發者一個錯誤的印象即他們能夠選擇忽略內存管理。這是一個巨大的錯誤github

即便是當使用高級語言的時候,開發者也應該要理解內存管理(或者至少是一些基礎)。有時候自動化內存管理會存在一些問題(好比垃圾回收中的 bugs 或者實施的侷限性等等),爲了可以合理地處理內存泄漏問題(或者以最小代價和代碼缺陷來尋找一個合適的方案),開發者就必須理解內存管理。web

內存生命週期

無論你使用哪一種編程語言,內存生命週期幾乎是同樣的:算法

如下是每一步生命週期所發生事情的一個概述:編程

  • 分配內存-內存是由操做系統分配,這樣程序就可使用它。在底層語言(例如 C 語言),開發者能夠顯式地操做內存。而在高級語言中,操做系統幫你處理。
  • 使用內存-這是程序實際使用以前分配的內存的階段。當你在代碼中使用已分配的變量的時候,就會發生內存讀寫的操做。
  • 釋放內存-該階段你能夠釋放你再也不使用的整塊內存,該內存就能夠被釋放且能夠被再利用。和內存分配操做同樣,該操做也是用底層語言顯式編寫的。

爲快速瀏覽調用堆棧和動態內存管理的概念,你能夠閱讀第一篇文章設計模式

啥是內存?

在直接跳向 JavaScript 內存管理以前,先來簡要地介紹一下內存及其工做原理。

從硬件層面看,計算機內存是由大量的 flip flops 所組成的(這裏大概查了下,即大量的二進制電路所組成的)。每一個 flip flop 包含少許晶體管並可以存儲一個比特位。單個的 flip flops 能夠經過一個惟一標識符尋址,因此就能夠讀和覆寫它們。所以,理論上,咱們能夠把整個計算機內存當作是由一個巨大的比特位數組所組成的,這樣就能夠進行讀和寫。

做爲猿類,咱們並不擅長用位來進行全部的邏輯思考和計算,因此咱們把位組織成一個更大的組,這樣就能夠用來表示數字。8 位稱爲一字節。除了字節還有字(16 或 32 位)。

內存中存儲着不少東西:

  • 全部變量及全部程序使用的其它數據。
  • 程序代碼,包括操做系統的代碼。

編譯器和操做系統一塊兒協做來爲你進行內存管理,可是建議你瞭解一下底層是如何實現的。

當編譯代碼的時候,編譯器會檢查原始數據類型並提早計算出程序運行所須要的內存大小。在所謂的靜態堆棧空間中,所需的內存大小會被分配給程序。這些變量所分配到的內存所在的空間之因此被稱爲靜態內存空間是由於當調用函數的時候,函數所需的內存會被添加到現存內存的頂部。當函數中斷,它們被以 LIFO(後進先出) 的順序移出內存。好比,考慮以下代碼:

int n; // 4 字節
int x[4]; // 4 個元素的數組,每一個數組元素 4 個字節
double m; // 8 字節
複製代碼

編譯器會當即計算出代碼所需的內存:4 + 4 x 4 + 8 = 28 字節。

編譯器是這樣處理當前整數和浮點數的大小的。大約 20 年前,整數通常是 2 字節而 浮點數是 4 字節。代碼不用依賴於當前基礎數據類型的字節大小。

編譯器會插入標記,標記會和操做系統協商從堆棧中獲取所須要的內存大小,以便在堆棧中存儲變量。

在以上示例中,編譯知道每一個變量的準確內存地址。事實上,當你編寫變量 n 的時候,會在內部把它轉換爲相似 "內存地址 412763" 的樣子。

注意到這裏當咱們試圖訪問 x[4] 時候,將會訪問到 m 相關的數據。這是由於咱們訪問了數組中不存在的數組元素-它超過了最後一個實際分配到內存的數組元素 x[3] 4 字節,而且有可能會讀取(或者覆寫) m 的位。這幾乎能夠肯定會產生其它程序所預料不到的後果。

當函數調用其它函數的時候,各個函數都會在被調用的時候取得其在堆棧中的各自分片內存地址。函數會把保存它全部的本地變量,但也會有一個程序計數器用來記住函數在其執行環境中的地址。當函數運行結束時,其內存塊能夠再次被用做其它用途。

動態內存分配

不幸的是,想要知道編譯時一個變量須要多少內存並無想象中那般容易。設想一下若要作相似以下事情:

int n = readInput(); // 從用戶讀取信息
...
// 建立一個含有 n 個元素的數組
複製代碼

這裏,編譯器並不知道編譯時數組須要多少內存,由於這是由用戶輸入的數組元素的值所決定的。

所以,就不可以在堆棧中爲變量分配內存空間。相反,程序須要在運行時顯式地從操做系統分配到正確的內存空間。這裏的內存是由動態內存空間所分配的。靜態和動態內存分配的差別總結以下圖表:

*靜態和動態分配內存的區別*

爲了徹底理解動態內存分配的工做原理,咱們須要花點時間瞭解指針,這個就可能有點跑題了 ^.^。若是你對指針感興趣,請留言,而後咱們將會在之後的章節中討論更多關於指針的內容。

JavaScript 中的內存分配

如今,咱們將會介紹在 JavaScript 中是如何分配內存的((第一步)。

JavaScript 經過聲明變量值,本身處理內存分配工做而不須要開發者干涉。

var n = 374; // 爲數字分配內存
var s = 'sessionstack'; // 爲字符串分配內存

var o = {
  a: 1,
  b: null
}; // 爲對象及其值分配內存

var a = [1, null, 'str']; // (相似對象)爲數組及其數組元素值分配內存

function f(a) {
  return a + 3;
} // 分配一個函數(一個可調用對象)

// 函數表達式也分配一個對象
someElement.addEventListener('click', function() {
  someElement.style.backgroundColor = 'blue';
}, false);
複製代碼

一些函數調用也會分配一個對象:

var d = new Date(); // 分配一個日期對象

var e = document.createElement('div'); // 分配一個 DOM 元素
複製代碼

能夠分配值或對象的方法:

var s1 = 'sessionstack';
var s2 = s1.substr(0, 3); // s2 爲一個新字符串
// 由於字符串是不可變的,因此 JavaScript 可能會選擇不分配內存而只是存儲數組 [0, 3] 的內存地址範圍。
var a1 = ['str1', 'str2'];
var a2 = ['str3', 'str4'];
var a3 = a1.concat(a2);
// 包含 4 個元素的新數組由 a1 和 a2 數組元素所組成
複製代碼

JavaScript 中的內存使用

JavaScript 中使用分配的內存主要指的是內存讀寫。

能夠經過爲變量或者對象屬性賦值,亦或是爲函數傳參來使用內存。

釋放再也不使用的內存

大多數的內存管理問題是出如今這一階段。

痛點在於檢測出什麼時候分配的內存是閒置的。它常常會要求開發者來決定程序中的這段內存是否已經再也不使用,而後釋放它。

高級程序語言集成了一塊稱爲垃圾回收器的軟件,該軟件的工做就是追蹤內存分配和使用狀況以便找出並自動釋放閒置的分配內存片斷。

不幸的是,這是個近似的過程,由於斷定一些內存片斷是否閒置的廣泛問題在於其不可斷定性(不能爲算法所解決)。

大多數的垃圾回收器會收集那些再也不被訪問的內存,好比引用該內存的全部變量超出了內存尋址範圍。然而仍是會有低於近似值的內存空間被收集,由於在任何狀況下仍然可能會有變量在內存尋址範圍內引用該內存地址,即便該內存是閒置的。

內存垃圾回收

因爲找出 "再也不使用" 的內存的不可斷定性,針對這一廣泛問題,垃圾回收實現了一個有限的解決方案。本小節將會闡述必要的觀點來理解主要的內存垃圾回收算法及其侷限性。

內存引用

引用是內存垃圾回收算法所依賴的主要概念之一。

在內存管理上下文中,若是對象 A 訪問了另外一個對象 B 表示 A 引用了對象 B(能夠隱式或顯式)。舉個栗子,一個 JavaScript 對象有引用了它的原型(隱式引用)和它的屬性值(顯式引用)。

在這個上下文中,"對象" 的概念被拓展超過了通常的 JavaScript 對象而且包含函數做用域(或者全局詞法做用域)。

詞法做用域定義瞭如何在嵌套函數中解析變量名。即便父函數已經返回,內部的函數仍然會包含父函數的做用域。

垃圾回收引用計數

這是最簡單的內存垃圾回收算法。當一個對象被 0 引用,會被標記爲 "可回收內存垃圾"。

看下以下代碼:

var o1 = {
  o2: {
    x: 1
  }
};

// 建立兩個對象。
// 'o1' 引用對象 'o2' 做爲其屬性。所有都是不可回收的。

// 'o3' 是第二個引用 'o1' 對象的變量
var o3 = o1;

o1 = 1; // 如今,原先在 'o1' 中的對象只有一個單一的引用,以變量 'o3' 來表示

// 引用對象的 'o2' 屬性。
// 該對象有兩個引用:一個是做爲屬性,另外一個是 'o4' 變量
var o4 = o3.o2;

// 'o1' 對象如今只有 0 引用,它能夠被做爲內存垃圾回收。
// 然而,其 'o2' 屬性仍然被變量 'o4' 所引用,因此它的內存不可以被釋放。
o3 = '374';

o4 = null;
// 'o1' 中的 'o2' 屬性如今只有 0 引用了。因此 'o1' 對象能夠被回收。
複製代碼

循環引用是個麻煩事

循環引用會形成限制。在如下的示例中,建立了兩個互相引用的對象,這樣就會形成循環引用。函數調用以後他們將會超出範圍,因此,實際上它們是無用且能夠釋放對他們的引用。然而,引用計數算法會認爲因爲兩個對象都至少互相引用一次,因此他們都不可回收的。

function f() {
  var o1 = {};
  var o2 = {};
  o1.P = O2; // O1 引用 o2
  o2.p = o1; // o2 引用 o1. 這就形成循環引用
}

f();
複製代碼

標記-清除算法

爲了判斷是否須要釋放對對象的引用,算法會肯定該對象是否可得到。

標記-清除算法包含三個步驟:

  • 根:通常來講,根指的是代碼中引用的全局變量。就拿 JavaScript 來講,window 對象便是根的全局變量。Node.js 中相對應的變量爲 "global"。垃圾回收器會構建出一份全部根變量的完整列表。
  • 隨後,算法會檢測全部的根變量及他們的後代變量並標記它們爲激活狀態(表示它們不可回收)。任何根變量所到達不了的變量(或者對象等等)都會被標記爲內存垃圾。
  • 最後,垃圾回收器會釋放全部非激活狀態的內存片斷而後返還給操做系統。

標記-清除算法的動態圖示

該算法比以前的算法要好,由於對象零引用可讓對象不可得到。反之則否則,正如以前所看到的循環引用。

從 2012 年起,全部的現代瀏覽器都內置了一個標記-清除垃圾回收器。前些年全部對於 JavaScript 內存垃圾收集(分代/增量/併發/並行 垃圾收集)的優化都是針對標記-清除算法的實現的優化,但既沒有提高垃圾收集算法自己,也沒有提高斷定對象是否可得到的能力。

你能夠查看這篇文章 來了解追蹤內存垃圾回收的詳情及包含優化了的標記-清除算法。

循環引用再也不讓人蛋疼

在以前的第一個示例中,當函數返回,全局對象再也不引用這兩個對象。結果,內存垃圾回收器發現它們是不可得到的。

即便兩個對象互相引用,也不可以從根變量得到他們。

內存垃圾回收器的反直觀行爲

雖然內存垃圾回收器很方便,可是它們也有其一系列的代價。其中之一即是不肯定性。意思即內存垃圾回收具備不可預見性。你不能肯定內存垃圾收集的確切時機。這意味着在某些狀況下,程序會使用比實際須要更多的內存。在其它狀況下,在特定的交互敏感的程序中,你也許須要注意那些內存垃圾收集短暫停時間。雖然不肯定性意味着不可以肯定何時能夠進行內存垃圾收集,可是大多數 GC 的實現都是在內存分配期間進行內存垃圾回收的通常模式。若是沒有進行內存分配,大多數的內存垃圾回收就會保持閒置狀態。考慮如下狀況:

  • 分配一段固定大小的內存。
  • 大多數的元素(或全部)被標記爲不可得到(假設咱們賦值咱們再也不須要的緩存爲 null )
  • 再也不分配其它內存。

在該狀況下,大多數的內存垃圾回收器不會再運行任何的內存垃圾回收。換句話說,即便能夠對該不可得到的引用進行垃圾回收,可是內存收集器不會進行標記。雖然這不是嚴格意義上的內存泄漏,可是這會致使高於日常的內存使用率。

內存泄漏是啥?

正如內存管理所說的那樣,內存泄漏即一些程序在過去時使用但處於閒置狀態,卻沒有返回給操做系統或者可用的內存池。

編程語言喜歡多種內存管理方法。然而,某個內存片斷是否被使用是一個不肯定的問題。換句話說,只有開發人員清楚某個內存片斷是否能夠返回給操做系統。

某些編程語言會爲開發者提供功能函數來解決這個問題。其它的編程語言徹底依賴於開發者全權掌控哪一個內存片斷是可回收的。維其百科上有關於手動自動內存管理的好文章。

四種常見的 JavaScript 內存泄漏

1: 全局變量

JavaScript 以一種有趣的方式來處理未聲明變量:當引用一個未聲明的變量,會在全局對象上建立一個新的變量。在瀏覽器中,全局對象是 window,這意味着以下代碼:

function foo(arg) {
  bar = "some text";
}
複製代碼

等同於:

function foo(arg) {
  window.bar = "some text";
}
複製代碼

變量 bar 本意是隻能在 foo 函數中被引用。可是若是你沒有用 var 來聲明變量,那麼將會建立一個多餘的全局變量。在上面的例子中,並不會形成大的事故。但你能夠很天然地想象一個更具破壞性的場景。

你也可使用 this 關鍵字不經意地建立一個全局變量。

function foo() {
  this.var1 = "potential accidental global";
}

// 調用 foo 函數自身,this 會指向全局對象(window)而不是未定義
複製代碼

你能夠經過在 JavaScript 文件的頂部添加 'use strict' 來避免以上的全部問題,'use strict' 會切換到更加嚴格的 JavaScript 解析模式,這樣就能夠防止建立意外的全局變量。

意外的全局變量的確是個問題,而代碼常常會被顯式定義的全局變量所污染,根據定義這些全局變量是不會被內存垃圾回收器所收集的。你須要特別注意的是使用全局變量來臨時存儲和處理大型的位信息。只有在必要的時候使用全局變量來存儲數據,記得一旦你再也不使用的時候,把它賦值爲 null 或者對其再分配。

2:定時器及被遺忘的回調函數

由於常常在 JavaScript 中使用 setInterval,因此讓咱們以它爲例。

框架中提供了觀察者和接受回調的其它指令一般會確保當他們的實例不可得到的時候,全部對回調的引用都會變成不可得到。很容易找到以下代碼:

var serverData = loadData();
setInterval(function() {
  var renderer = document.getElementById('renderer');
  if (renderer) {
    renderer.innerHTML = JSON.stringify(serverData);
  }
}, 5000); // 這將會每隔大約 5 秒鐘執行一次
複製代碼

以上代碼片斷展現了使用定時器來引用再也不須要的節點或數據的後果。

renderer 對象會在某些時候被替換或移除,這樣就會致使由定時處理程序封裝的代碼變得冗餘。當這種狀況發生的時候,無論是定時處理程序仍是它的依賴都不會被垃圾回收,這是因爲須要先中止定時器(記住,定時器仍然處於激活狀態)。這能夠歸結爲保存和處理數據加載的 serverData 變量也不會被垃圾回收。

當使用觀察者的時候,你須要確保一旦你再也不須要它們的時候顯式地移除它們(再也不須要觀察者或者對象變得不可得到)。

幸運的是,大多數現代瀏覽器都會替你進行處理:當被觀察者對象變得不可得到時,即便你忘記移除事件監聽函數,瀏覽器也會自動回收觀察者處理程序。之前,一些老掉牙的瀏覽器處理不了這些狀況(如老舊的 IE6)。

那麼,最佳實踐是當對象被廢棄的時候,移除觀察者處理程序。查看以下例子:

var element = document.getElementById('launch-button');
var counter = 0;

function onClick(event) {
  counter++;
  element.innerHTML = 'text' + counter;
}

element.addEventListener('click', onClick);

// Do stuff
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// 如今當元素超出範圍
// 即便在不能很好處理循環引用的瀏覽器中也會回收元素和 onClick 事件
複製代碼

在讓一個 DOM 節點不可得到以前,你再也不須要調用 removeEventListener,由於現代瀏覽器支持用內存垃圾回收器來檢測並適當地處理 DOM 節點的生命週期。

若是你使用 jQuery API(其它的庫和框架也支持的 API),你能夠在廢棄節點以前移除事件監聽函數。jQuery 也會確保即便在老舊的瀏覽器之中,也不會產生內存泄漏。

閉包

閉包是 JavaScript 的一個重要功能:嵌套函數能夠訪問外部(封閉)函數的變量。鑑於 JavaScript 運行時的實現細節,如下方法可能會形成內存泄漏:

var theThing = null

var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // 引用 originalThing
      console.log("hi");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log("message");
    }
  };
};
setInterval(replaceThing, 1000);
複製代碼

當調用 replaceThing 的時候,theThing 對象由一個大數組和新的閉包(someMethod)所組成。而 originalThingunused 變量建立的閉包所引用(即引用 replaceThing 函數以前的 theThing 變量)。須要記住的是當一旦爲同一個父做用域中的閉包建立閉包做用域的時候,該閉包做用域是共享的。

在這樣的狀況下,閉包 someMethodunused 共享相同的做用域。unused 引用了 origintalThing。即便 unused 永不使用,也能夠在 replaceThing 的做用域外使用 someMethod 函數。而後因爲 someMethodunused 共享相同的閉包做用域,unused 變量引用 originalThing 會強迫 unused 保持激活狀態(兩個閉包共享做用域)。這會阻止內存垃圾回收。

在以上例子中,閉包 someMethodunused 共享做用域,而 unused 引用 origintalThing。能夠在 replaceThing 做用域外經過 theThing 使用 someMethod,即便 unused 從未被使用。事實上,因爲 someMethodunused 共享閉包做用域,unused 引用 origintalThing 要求 unused 保持激活狀態。

全部的這些行爲會致使內存泄漏。當你不斷地運行如上代碼片斷,你將會發現飆升的內存使用率。當內存垃圾回收器運行的時候,這些內存使用率不會降低。這裏會建立出一份閉包鏈表(當前狀況下,其根變量是 theThing),每一個閉包做用域都間接引用了大數組。

該問題是由 Metor 小組發現的而且他們寫了一篇很好的文章來詳細描述該問題。

4: 源自 DOM 引用

有時候,開發者會在數據結構中存儲 DOM 節點。

假設你想要快速更新幾行表格內容。若是你在一個字典或者數組中保存對每一個表格行的引用,這將會形成重複引用相同的 DOM 元素:一個在 DOM 樹中而另外一個在字典中。若是你想要釋放對這些表格行的引用,你須要記得讓這些引用變成不可得到。

var elements = {
  button: document.getElementById('button'),
  image: document.getElementById('image')
};

function doStuff() {
  elements.image.src = 'http://example.com/image_name.png';
}

function removeImage() {
    // image 元素是 body 元素的直系後代元素
    document.body.removeChild(document.getElementById('image'));
    // 這時,咱們仍然在 elements 全局對象中引用了 #button 元素
    // 換句話說,按鈕元素仍然在內存中且不可以被垃圾回收器收集
}
複製代碼

你還須要額外考慮的狀況是引用 DOM 樹中的內節點或者葉節點。若是你在代碼中保存着對一個單元格的引用,這時候當你決定從 DOM 中移除表格,卻仍然會保持對該單元格的引用,這就會致使大量的內存泄漏。你能夠認爲內存垃圾回收器將會釋放除了該單元格之外的內存。而這還沒完。由於單元格是表格的一個後代元素然後代元素保存着對其父節點的引用,對一個單元格的引用會致使沒法釋放整個表格所佔用的內存

內存管理心得

如下內容爲我的原創分享。By 三月

指導思想

儘量減小內存佔用,儘量減小 GC。

  • 減小 GC 次數

    瀏覽器會不定時回收垃圾內存,稱爲 GC,不定時觸發,通常在向瀏覽器申請新內存時,瀏覽器會檢測是否到達一個臨界值再進行觸發。通常來講,GC 會較爲耗時,GC 觸發時可能會致使頁面卡頓及丟幀。故咱們要儘量避免GC的觸發。GC 沒法經過代碼觸發,但部分瀏覽器如 Chrome,可在 DevTools -> TimeLine 頁面手動點擊 CollectGarbage 按鈕觸發 GC。

  • 減小內存佔用

    下降內存佔用,可避免內存佔用過多致使的應用/系統卡頓,App 閃退等,在移動端尤其明顯。當內存消耗較多時,瀏覽器可能會頻繁觸發 GC。而如前所述,GC 發生在申請新內存時,若能避免申請新內存,則可避免GC 觸發。

優化方案

使用對象池

對象池**(英語:object pool pattern)是一種設計模式。**一個對象池包含一組已經初始化過且可使用的對象,而能夠在有需求時建立和銷燬對象。池的用戶能夠從池子中取得對象,對其進行操做處理,並在不須要時歸還給池子而非直接銷燬它。這是一種特殊的工廠對象。

若初始化、實例化的代價高,且有需求須要常常實例化,但每次實例化的數量較少的狀況下,使用對象池能夠得到顯著的效能提高。從池子中取得對象的時間是可預測的,但新建一個實例所需的時間是不肯定。

以上摘自維基百科。

使用對象池技術能顯著優化需頻繁建立對象時的內存消耗,但建議按不一樣使用場景作如下細微優化。

  1. 按需建立

    默認建立空對象池,按需建立對象,用完歸還池子。

  2. 預建立對象

    如在高頻操做下,如滾動事件、TouchMove事件、resize事件、for 循環內部等頻繁建立對象,則可能會觸發GC的發生。故在特殊狀況下,可優化爲提早建立對象放入池子。

    高頻狀況下,建議使用截流/防抖及任務隊列相關技術。

  3. 定時釋放

    對象池內的對象不會被垃圾回收,若極端狀況下建立了大量對象回收進池子卻不釋放只會拔苗助長。

    故池子需設計定時/定量釋放對象機制,如以已用容量/最大容量/池子使用時間等參數來定時釋放對象。

其餘優化tips

  1. 儘量避免建立對象,非必要狀況下避免調用會建立對象的方法,如 Array.sliceArray.mapArray.filter、字符串相加、$('div')ArrayBuffer.slice 等。

  2. 再也不使用的對象,手動賦爲 null,可避免循環引用等問題。

  3. 使用 Weakmap

  4. 生產環境勿用 console.log 大對象,包括 DOM、大數組、ImageData、ArrayBuffer 等。由於 console.log 的對象不會被垃圾回收。詳見Will console.log prevent garbage collection?

  5. 合理設計頁面,按需建立對象/渲染頁面/加載圖片等。

    • 避免以下問題:

      • 爲了省事兒,一次性請求所有數據。
      • 爲了省事兒,一次性渲染所有數據,再作隱藏。
      • 爲了省事兒,一次性加載/渲染所有圖片。
    • 使用重複 DOM 等,如重複使用同一個彈窗而非建立多個。

      如 Vue-Element 框架中,PopOver/Tooltip 等組件用於表格內時會建立 m * n 個實例,可優化爲只建立一個實例,動態設置位置及數據。

  6. ImageData 對象是 JS 內存殺手,避免重複建立 ImageData 對象。

  7. 重複使用 ArrayBuffer。

  8. 壓縮圖片、按需加載圖片、按需渲染圖片,使用恰當的圖片尺寸、圖片格式,如 WebP 格式。

圖片處理優化

假設渲染一張 100KB 大小,300x500 的透明圖片,粗略的可分爲三個過程:

  1. 加載圖片

    加載圖片二進制格式到內存中並緩存,此時消耗了100KB 內存 & 100KB 緩存。

  2. 解碼圖片

    將二進制格式解碼爲像素格式,此時佔用寬 * 高 * 24(透明爲32位)比特大小的內存,即 300 * 500 * 32,約等於 585 KB,這裏約定名爲像素格式內存。我的猜想此時瀏覽器會回收加載圖片時建立的 100KB 內存。

  3. 渲染圖片

    經過 CPU 或 GPU 渲染圖片,若爲 GPU 渲染,則還需上傳到 GPU 顯存,該過程較爲耗時,由圖片尺寸 / 顯存位寬決定,圖片尺寸越大,上傳時間越慢,佔用顯存越多。

其中,較舊的瀏覽器如Firefox回收像素內存時機較晚,若渲染了大量圖片時會內存佔用太高。

PS:瀏覽器會複用同一份圖片二進制內存及像素格式內存,瀏覽器渲染圖片會按如下順序去獲取數據:

顯存 >> 像素格式內存 >> 二進制內存 >> 緩存 >> 從服務器獲取。咱們需控制和優化的是二進制內存及像素內存的大小及回收。

總結一下,瀏覽器渲染圖片時所消耗內存由圖片文件大小內存、寬高、透明度等所決定,故建議:

  1. 使用 CSS三、SVG、IconFont、Canvas 替代圖片。展現大量圖片的頁面,建議使用 Canvas 渲染而非直接使用img標籤。具體詳見 Javascript的Image對象、圖像渲染與瀏覽器內存兩三事

  2. 適當壓縮圖片,可減少帶寬消耗及圖片內存佔用。

  3. 使用恰當的圖片尺寸,即響應式圖片,爲不一樣終端輸出不一樣尺寸圖片,勿使用原圖縮小代替 ICON 等,好比一些圖片服務如 OSS。

  4. 使用恰當的圖片格式,如使用WebP格式等。詳細圖片格式對比,使用場景等建議查看web前端圖片極限優化策略

  5. 按需加載及按需渲染圖片。

  6. 預加載圖片時,切記要將 img 對象賦爲 null,不然會致使圖片內存沒法釋放。

    當實際渲染圖片時,瀏覽器會從緩存中再次讀取。

  7. 將離屏 img 對象賦爲 null,src 賦爲 null,督促瀏覽器及時回收內存及像素格式內存。

  8. 將非可視區域圖片移除,須要時再次渲染。和按需渲染結合時實現很簡單,切換 src 與 v-src 便可。

參考連接:

garbage-collector-friendly-code/

移動 WEB 通用優化策略介紹(二)

H5前端性能優化高級進階

Javascript的Image對象、圖像渲染與瀏覽器內存兩三事

web前端圖片極限優化策略

MDN Weakmap

函數節流、函數防抖實現原理分析

a-tour-of-v8-garbage-collection

打個廣告 ^.^

今日頭條招人啦!發送簡歷到 likun.liyuk@bytedance.com ,便可走快速內推通道,長期有效!國際化PGC部門的JD以下:c.xiumi.us/board/v5/2H…,也可內推其餘部門!

本系列持續更新中,Github 地址請查閱這裏

相關文章
相關標籤/搜索