如何排查網(wǎng)頁在哪里發(fā)生了內(nèi)存泄漏?
大家好,我是前端西瓜哥。
今天我們來學習用 devtool 的 Performance 和 Memory 工具來找出網(wǎng)頁哪里發(fā)生了內(nèi)存泄漏。
Performace 面板
首先我們打開瀏覽器的 devtool,選擇 Performance(性能)面板,然后將 Memory 選項勾選上。不勾選的話,就不會記錄內(nèi)存使用情況,內(nèi)存泄漏分析就無從說起了。
然后進行性能數(shù)據(jù)收集:
- 點擊左上角的 “錄制” 按鈕(一個灰色的圓形),或者點它旁邊的 “刷新” 按鈕,會重新加載頁面并開始記錄,這樣就不用手動刷新然后手忙腳亂地點錄制按鈕了;
- 在頁面上執(zhí)行可能發(fā)生內(nèi)存泄漏的操作,比如打開一個彈窗,然后再關(guān)閉;
- 差不多了就再點擊 “錄制” 按鈕,結(jié)束錄制,然后出現(xiàn)下面圖片的結(jié)果。
查看內(nèi)存指標
看看內(nèi)存的使用情況。有這么幾步:
- 選中要分析的范圍;
- 選中 Main(主線程)。只有選中的話,內(nèi)存圖表才能顯示主線程對應(yīng)的信息;
- 查看內(nèi)存圖表的指標。
內(nèi)存圖表是一些折線圖,記錄了內(nèi)存指標隨時間發(fā)生的變化。這些內(nèi)存指標有:JS 堆內(nèi)存、Document 數(shù)、節(jié)點數(shù)、綁定監(jiān)聽器數(shù)量、GPU 內(nèi)存。
點擊它們可顯示或隱藏對應(yīng)的折線圖。
對于 JS Heap(11.9MB - 25.6MB) ,它表示的是在當前時間范圍內(nèi),JS 堆內(nèi)存最小值為 11.9 MB,最大為 25.6 MB。
將光標懸停在折線圖上,可以看到對應(yīng)的值:
查看內(nèi)存下限的變化
內(nèi)存會增長是正常的現(xiàn)象。比如我們調(diào)用函數(shù),會創(chuàng)建一些臨時變量,導致內(nèi)存升高。函數(shù)執(zhí)行完,這些變量就沒用了,但不會馬上回收,而是會在適當?shù)臅r機進行內(nèi)存回收,將內(nèi)存再降下去。
臨時分配的短命內(nèi)存我們并不關(guān)心,我們更關(guān)注的是一些常駐的內(nèi)存,對應(yīng)的要看的是 內(nèi)存下限的變化。
如果內(nèi)存下限不斷上升,說明常駐內(nèi)存變大了。大多數(shù)情況下是正常的,比如:
- 調(diào)用函數(shù),將函數(shù)返回的結(jié)果進行緩存;
- 創(chuàng)建新的組件。
也可能是內(nèi)存泄漏了。
當懷疑是內(nèi)存泄漏時,我們就可以使用 Memory 面板記錄快照,做進一步的排查。
Memory 面板
打開 Memory 面板,點擊左上角的 “錄制按鈕”,生成當前時刻的堆內(nèi)存快照。然后通過快照了解 JS 對象的內(nèi)存分布
Summary View
快照結(jié)果默認會展示為 概要視圖(Summary View)。
這個表格的表格項是基于構(gòu)造函數(shù)進行歸類的。可以看到有不少原生的構(gòu)造函數(shù),還有一堆閉包。
每個項有以下幾個屬性:
- Constructor:構(gòu)造函數(shù)。對于沒有構(gòu)造函數(shù)的字面量,用類似(string)? 、(array) 的表示。
- Distance:到根節(jié)點的最短路徑。
- Shallow Size:自己占用的內(nèi)存大小,不包括它引入的其他對象內(nèi)存,單位為字節(jié)。
- Retained Size:對象自己以及它引用的對象的內(nèi)存,單位也是字節(jié)。
- Object Count:對象數(shù)量,就是 Constructor 名旁邊那個數(shù)字。
上面是默認的 Summary View 視圖。
除了它,我們還有其他的視圖,可以像下面這樣進行視圖類型的切換。
Comparison View
比較視圖(Comparison View)則是用來比較兩個快照的變化。
這里我選中了快照 3,然后將對比快照設(shè)置為 快照 1。
這個表格表示從快照 1 變成快照 3 發(fā)生的變化。沒有發(fā)生變化的項不會進行展示。
字段有:
- Constructor:構(gòu)造函數(shù)。
- #New:新增的對象數(shù)量。
- #Deleted:刪除的對象數(shù)量。
- #Delta:總體上的對象變化數(shù)量。
- Alloc.Size:分配的總內(nèi)存。
- Freed Size:釋放了多少內(nèi)存。
- Size Delta:總體上的內(nèi)存變化。
Containment View
該視圖可以讓我們從根節(jié)點為起點,往下去查看各種對象占用的內(nèi)存,以及被創(chuàng)建的代碼位置等信息。
字段:
- Object:普通對象或者 DOM 節(jié)點:
- Distance:到根節(jié)點的距離。
- Shallow Size:對象大小,不計算引用的對象。
- Retained Size:對象大小,但其引用的對象大小也計算在內(nèi)。
Statistics View
圓環(huán)統(tǒng)計表。
各種內(nèi)存類型的占總內(nèi)存的百分比情況。
使用 Memory 面板注意事項
盡量減少干擾項的影響力。
- 分辨正常的內(nèi)存變化會的干擾。
- 注意開發(fā)環(huán)境的打包器熱加載邏輯等的影響。
- 生成環(huán)境的代碼是混淆過的,一些構(gòu)造器名字很奇怪,如果可以的話,本地打包一份沒經(jīng)過混淆過的代碼做 debug。或者也可以 hover 看看對象結(jié)構(gòu)猜測對應(yīng)構(gòu)造器,但效率不高。
- 不要有瀏覽器插件,它們也占用和影響內(nèi)存,可以用無痕瀏覽器。
常見內(nèi)存泄漏原因和排查
忘記及時取消監(jiān)聽器綁定
新手老鳥都容易犯的錯誤,就是 忘記及時取消監(jiān)聽器綁定。它會導致:
- 監(jiān)聽器函數(shù)中的對象遲遲不能釋放,比如非常大的組件實例。
- 綁定大量無用的監(jiān)聽器函數(shù)。
怎么排查?
如果監(jiān)聽器是綁定到 DOM 中,我們可以不斷執(zhí)行可以看 Listener 數(shù)量的變化。
我寫了個彈窗組件,它會在掛載時給 document.body 注冊一個函數(shù),然后這個函數(shù)會用到這個組件下的變量。但銷毀時不取消注冊。
打開 Performance 面板,錄制,然后不停打開和關(guān)閉彈窗,然后結(jié)束錄制。我們就能看這個 Listeners 的數(shù)量的變化,不斷地變高那就是忘了。
也可以看看 Memoery 面板中 Comparison View 的快照對比中,EventListener 數(shù)量的變化:
具體是哪個,可以看 EventListener 下的最后幾個對象。
點擊這個藍色的鏈接,就能跳到對應(yīng)的代碼位置:
此外,還可以用 Chrome 控制臺提供的 getEventListeners(element) 方法,它會返回一個元素事件綁定的函數(shù)有哪些。這個方法不是標準方法,是 Chrome 自帶的工具方法,只能在控制臺上用。我們可以寫個方法,從根節(jié)點往下找,找出綁定函數(shù)數(shù)量最多的節(jié)點,這個節(jié)點多得離譜那就大概率是忘了解綁。
如果不是 DOM 上的監(jiān)聽器,比如發(fā)布訂閱庫的事件集合,那就要看構(gòu)造器對應(yīng)對象數(shù)量的變化了。
閉包
閉包就是拿到函數(shù) A 內(nèi)的另一個函數(shù) B,函數(shù) B 會捕獲到函數(shù) A 作用域中的變量。
這個就導致了對一些對象的隱式引用,比如一個 DOM 元素。我們需要在不需要使用時將其設(shè)置為 null。
我們可以看看有沒有什么 Detached 的元素。Detached 表示不在當前文檔樹上,如果持續(xù)增多,可能發(fā)生了內(nèi)存泄漏。
說真的閉包是一個正常的特性,沒理由和內(nèi)存泄漏有關(guān)才是。
函數(shù) B 被持有不銷毀,自然它捕獲的函數(shù) A 中的變量就不能銷毀,和對象里有一些屬性,這些屬性不能銷毀沒啥區(qū)別。函數(shù) B 銷毀了,對應(yīng)的變量自然也就回收了。
有空我再研究下寫篇專題。
console
“你到底都打印了些什么啊?”
還有個比較常見的就是,在開發(fā)的時候用 console 打印一些對象,合并到主分支又忘記去掉。這些對象是不會被回收的,因為開發(fā)者可能會去控制臺看看這些對象的內(nèi)容。這在打印大量大對象時會出性能問題。
排查方法很簡單,去看 DevTool 的控制臺輸出了什么內(nèi)容,看看有沒有大對象。
一些有助于 debug 的 console 是有必要的,但不要濫用。
集合類型的緩存爆炸
我們經(jīng)常用對象、數(shù)組、Map、Set 等集合類型,去做數(shù)據(jù)的緩存。
當緩存大量對象時,會占用大量的內(nèi)存,但其中有不少內(nèi)容是不需要用的。對于前端來說,內(nèi)存不像后端那樣純金寸土,動不動就是大批量數(shù)據(jù)要處理,緩存使用起來挺隨意的。
對于緩存問題,還要要有點意識,我們可以:
- 使用 LRU 算法,將最久沒使用的緩存移除,控制緩存數(shù)量;
- 設(shè)置緩存過期時間;
- 對于臨時緩存,考慮使用 WeakMap 和 WeakSet,它們會在 GC 時強制回收;
這些就沒啥好分析的,就看看內(nèi)存下限變化,某些對象是否變大變多了。
結(jié)尾
今天帶大家簡單入門了 devtool 提供的內(nèi)存分析工具,但光說不練假把式,還是要多多實戰(zhàn)。