天天看點

如何排查網頁在哪裡發生了記憶體洩漏?

作者:前端西瓜哥

大家好,我是前端西瓜哥。

今天我們來學習用 devtool 的 Performance 和 Memory 工具來找出網頁哪裡發生了記憶體洩漏。

Performace 面闆

首先我們打開浏覽器的 devtool,選擇 Performance(性能)面闆,然後将 Memory 選項勾選上。不勾選的話,就不會記錄記憶體使用情況,記憶體洩漏分析就無從說起了。

然後進行性能資料收集:

  1. 點選左上角的 “錄制” 按鈕(一個灰色的圓形),或者點它旁邊的 “重新整理” 按鈕,會重新加載頁面并開始記錄,這樣就不用手動重新整理然後手忙腳亂地點錄制按鈕了;
  2. 在頁面上執行可能發生記憶體洩漏的操作,比如打開一個彈窗,然後再關閉;
  3. 差不多了就再點選 “錄制” 按鈕,結束錄制,然後出現下面圖檔的結果。
如何排查網頁在哪裡發生了記憶體洩漏?

檢視記憶體名額

看看記憶體的使用情況。有這麼幾步:

  1. 選中要分析的範圍;
  2. 選中 Main(主線程)。隻有選中的話,記憶體圖表才能顯示主線程對應的資訊;
  3. 檢視記憶體圖表的名額。
如何排查網頁在哪裡發生了記憶體洩漏?

記憶體圖表是一些折線圖,記錄了記憶體名額随時間發生的變化。這些記憶體名額有:JS 堆記憶體、Document 數、節點數、綁定監聽器數量、GPU 記憶體。

點選它們可顯示或隐藏對應的折線圖。

對于 JS Heap(11.9MB - 25.6MB) ,它表示的是在目前時間範圍内,JS 堆記憶體最小值為 11.9 MB,最大為 25.6 MB。

将光标懸停在折線圖上,可以看到對應的值:

如何排查網頁在哪裡發生了記憶體洩漏?

檢視記憶體下限的變化

記憶體會增長是正常的現象。比如我們調用函數,會建立一些臨時變量,導緻記憶體升高。函數執行完,這些變量就沒用了,但不會馬上回收,而是會在适當的時機進行記憶體回收,将記憶體再降下去。

臨時配置設定的短命記憶體我們并不關心,我們更關注的是一些常駐的記憶體,對應的要看的是 記憶體下限的變化。

如何排查網頁在哪裡發生了記憶體洩漏?

如果記憶體下限不斷上升,說明常駐記憶體變大了。大多數情況下是正常的,比如:

  1. 調用函數,将函數傳回的結果進行緩存;
  2. 建立新的元件。

也可能是記憶體洩漏了。

當懷疑是記憶體洩漏時,我們就可以使用 Memory 面闆記錄快照,做進一步的排查。

Memory 面闆

打開 Memory 面闆,點選左上角的 “錄制按鈕”,生成目前時刻的堆記憶體快照。然後通過快照了解 JS 對象的記憶體分布

如何排查網頁在哪裡發生了記憶體洩漏?

Summary View

快照結果預設會展示為 概要視圖(Summary View)。

如何排查網頁在哪裡發生了記憶體洩漏?

這個表格的表格項是基于構造函數進行歸類的。可以看到有不少原生的構造函數,還有一堆閉包。

每個項有以下幾個屬性:

  • Constructor:構造函數。對于沒有構造函數的字面量,用類似 (string) 、(array) 的表示;
  • Distance:到根節點的最短路徑;
  • Shallow Size:自己占用的記憶體大小,不包括它引入的其他對象記憶體,機關為位元組;
  • Retained Size:對象自己以及它引用的對象的記憶體,機關也是位元組;
  • Object Count:對象數量,就是 Constructor 名旁邊那個數字;

上面是預設的 Summary View 視圖。

除了它,我們還有其他的視圖,可以像下面這樣進行視圖類型的切換。

如何排查網頁在哪裡發生了記憶體洩漏?

Comparison View

比較視圖(Comparison View)則是用來比較兩個快照的變化。

如何排查網頁在哪裡發生了記憶體洩漏?

這裡我選中了快照 3,然後将對比快照設定為 快照 1。

這個表格表示從快照 1 變成快照 3 發生的變化。沒有發生變化的項不會進行展示。

字段有:

  • Constructor:構造函數;
  • #New:新增的對象數量;
  • #Deleted:删除的對象數量;
  • #Delta:總體上的對象變化數量;
  • Alloc.Size:配置設定的總記憶體;
  • Freed Size:釋放了多少記憶體;
  • Size Delta:總體上的記憶體變化;

Containment View

該視圖可以讓我們從根節點為起點,往下去檢視各種對象占用的記憶體,以及被建立的代碼位置等資訊。

如何排查網頁在哪裡發生了記憶體洩漏?

字段:

  • Object:普通對象或者 DOM 節點:
  • Distance:到根節點的距離;
  • Shallow Size:對象大小,不計算引用的對象;
  • Retained Size:對象大小,但其引用的對象大小也計算在内;

Statistics View

圓環統計表。

各種記憶體類型的占總記憶體的百分比情況。

如何排查網頁在哪裡發生了記憶體洩漏?

使用 Memory 面闆注意事項

盡量減少幹擾項的影響力。

  1. 分辨正常的記憶體變化會的幹擾;
  2. 注意開發環境的打包器熱加載邏輯等的影響;
  3. 生成環境的代碼是混淆過的,一些構造器名字很奇怪,如果可以的話,本地打包一份沒經過混淆過的代碼做 debug。或者也可以 hover 看看對象結構猜測對應構造器,但效率不高。
  4. 不要有浏覽器插件,它們也占用和影響記憶體,可以用無痕浏覽器。

常見記憶體洩漏原因和排查

忘記及時取消監聽器綁定

新手老鳥都容易犯的錯誤,就是 忘記及時取消監聽器綁定。它會導緻:

  1. 監聽器函數中的對象遲遲不能釋放,比如非常大的元件執行個體;
  2. 綁定大量無用的監聽器函數。

怎麼排查?

如果監聽器是綁定到 DOM 中,我們可以不斷執行可以看 Listener 數量的變化。

我寫了個彈窗元件,它會在挂載時給 document.body 注冊一個函數,然後這個函數會用到這個元件下的變量。但銷毀時不取消注冊。

打開 Performance 面闆,錄制,然後不停打開和關閉彈窗,然後結束錄制。我們就能看這個 Listeners 的數量的變化,不斷地變高那就是忘了。

如何排查網頁在哪裡發生了記憶體洩漏?

也可以看看 Memoery 面闆中 Comparison View 的快照對比中,EventListener 數量的變化:

如何排查網頁在哪裡發生了記憶體洩漏?

具體是哪個,可以看 EventListener 下的最後幾個對象。

如何排查網頁在哪裡發生了記憶體洩漏?

點選這個藍色的連結,就能跳到對應的代碼位置:

如何排查網頁在哪裡發生了記憶體洩漏?

此外,還可以用 Chrome 控制台提供的 getEventListeners(element) 方法,它會傳回一個元素事件綁定的函數有哪些。這個方法不是标準方法,是 Chrome 自帶的工具方法,隻能在控制台上用。我們可以寫個方法,從根節點往下找,找出綁定函數數量最多的節點,這個節點多得離譜那就大機率是忘了解綁。

如果不是 DOM 上的監聽器,比如釋出訂閱庫的事件集合,那就要看構造器對應對象數量的變化了。

閉包

閉包就是拿到函數 A 内的另一個函數 B,函數 B 會捕獲到函數 A 作用域中的變量。

這個就導緻了對一些對象的隐式引用,比如一個 DOM 元素。我們需要在不需要使用時将其設定為 null。

我們可以看看有沒有什麼 Detached 的元素。Detached 表示不在目前文檔樹上,如果持續增多,可能發生了記憶體洩漏。

如何排查網頁在哪裡發生了記憶體洩漏?

說真的閉包是一個正常的特性,沒理由和記憶體洩漏有關才是。

函數 B 被持有不銷毀,自然它捕獲的函數 A 中的變量就不能銷毀,和對象裡有一些屬性,這些屬性不能銷毀沒啥差別。函數 B 銷毀了,對應的變量自然也就回收了。

有空我再研究下寫篇專題。

console

“你到底都列印了些什麼啊?”

還有個比較常見的就是,在開發的時候用 console 列印一些對象,合并到主分支又忘記去掉。這些對象是不會被回收的,因為開發者可能會去控制台看看這些對象的内容。這在列印大量大對象時會出性能問題。

排查方法很簡單,去看 DevTool 的控制台輸出了什麼内容,看看有沒有大對象。

一些有助于 debug 的 console 是有必要的,但不要濫用。

集合類型的緩存爆炸

我們經常用對象、數組、Map、Set 等集合類型,去做資料的緩存。

當緩存大量對象時,會占用大量的記憶體,但其中有不少内容是不需要用的。對于前端來說,記憶體不像後端那樣純金寸土,動不動就是大批量資料要處理,緩存使用起來挺随意的。

對于緩存問題,還要要有點意識,我們可以:

  1. 使用 LRU 算法,将最久沒使用的緩存移除,控制緩存數量;
  2. 設定緩存過期時間;
  3. 對于臨時緩存,考慮使用 WeakMap 和 WeakSet,它們會在 GC 時強制回收;

這些就沒啥好分析的,就看看記憶體下限變化,某些對象是否變大變多了。

結尾

今天帶大家簡單入門了 devtool 提供的記憶體分析工具,但光說不練假把式,還是要多多實戰。

我是前端西瓜哥,歡迎關注我,學習更多前端知識。

繼續閱讀