單頁面應用下的JS內存管理(2)--實戰

閱讀本文需要了解chrome dev tool的timeline跟profile

在上一篇文章單頁面應用下的JS內存管理(1)中,我主要介紹了容易引起致命內存泄漏的地方,先謝謝諸位知友的贊以及鼓勵。這一個月中,也一直在構思如何寫好下篇。本來想介紹下之前項目中排查內存泄漏的經驗,但這樣的話,就得使用之前錯誤的代碼進行模擬,再一個就是之前的很多排查都是基於自己對項目本身的了解,寫出來不一定很直觀,讀者也不能跟著進行實踐操作。

在上篇中有一個非常好的例子網易有數,網易有數是一個非常優秀的數據可視化工具,可能是因為我提到的操作並不是普遍操作,對用戶體驗還沒有太大影響,並沒有引起開發測試的注意。本文將通過定位網易有數的內存泄漏來介紹如何排查內存泄漏,因為對項目架構不了解,再加上都是壓縮後的代碼,還是頗有難度的。(我這麼大無畏地幫助別人定位置bug,海波老師是不是該發個紅包啊......)

上文提到,很多內存泄漏發生在頁面的局部刷新的時候,我懷疑其在頁面刷新時會有內存泄漏

通過timeline查看整體內存結果

打開頁面報表,點開timeline的錄製按鈕,點擊刷新按鈕,刷新頁面數據。從下圖timeline的分析數據可以看出,每次刷新後雖然有部分內存釋放,但是總量卻是穩步增加,並且nodes數也一樣穩步增加。這時候就可以懷疑:有內存泄漏,且有部分DOM節點沒能釋放。

通過profile定為內存泄漏點

通過timeline,我們懷疑發生了內存泄漏,下面我們就通過proflie去定為內存泄漏點。

重新刷新頁面,用profile記錄下初始內存快照,多次頁面刷新後再次紀錄下內存快照。

chrome的profile提供了四種視圖:

  • Summary — 分類總覽視圖
  • Comparison — 對比兩次內存快照的結果視圖
  • Containment — 按照以GC root,全局對象的總覽視圖
  • Dominators — 分類佔比圖(看看就好)

為什麼要錄下兩次的內存快照呢?當然是比較兩次內容,看看第二次到底多了哪些內容,我們自然選擇Comparison視圖,對比Snap 2與Snap 1,按照allocation size進行排序。(熟悉項目的也可以從detached DOM tree入手)

剛剛我刷新了10幾次頁面,因為是demo數據,十幾次的數據都是一樣的。也就是說應該會有很多組有10幾個相同大小的內存塊,上文提到這正是比較致命的內存泄漏(比如上圖中大概10個12344大小的內存塊)。我們便從這些入手進行分析:

看下這10個對象的Retainers視圖, Retainers視圖有四列

  • Object — 顯示該object到GC Root的詳細路徑
  • Distance— 到GC Root的最短路徑
  • Shallow Size— 該對象直接佔有的內存(因為js大多數都是引用,所以直接佔有比較小)
  • Retained Size— 一旦該對象被刪除,能被釋放的內存量

查看幾組詳細路徑後,我一開始以為是chart引起的,但是chart的distance卻高達11,而且這幾組都有「_events.clickouter」這個相同的引用,會不會是事件委託造成的內存泄漏呢?

hover到「clickouter」上(上圖),我們可以定位到以下代碼。說實話,我也被下面函數的引用關係給繞暈了。

註冊一個clickouter處理,會在t變數push一個處理函數,同時返回一個處理後事函數,一旦用不到了,執行後處理後事。會不會是註冊了clickouter,但是忘記了在周期結束後解綁呢?

Regular.event("clickouter", function() { var t = []; var e = function(e) { if (t.length) t.forEach(function(t) { if ("function" == typeof t) t(e) }) }; var i = function(t) { return function(e) { return e !== t && !t.contains(e) } }; return function n(s, o) { function l(t) { if (c(t.target)) o(t) } var c = i(s); var d = t.length; t.push(l); if (!d) setTimeout(function() { a.on(document, !r ? "mousedown" : "touchstart", e) }, 10); return function u() { var i = t.indexOf(l); if (~i) t.splice(i, 1); if (!t.length) a.off(document, !r ? "mousedown" : "touchstart", e) } } }(

我們在這裡打個斷點看看,發現每次刷新,t的length都會增加12,我再打個斷點在u()上,發現每次刷新都不會執行。看來我的懷疑是對的,為了驗證我的想法,在執行過程中手動清空t變數(t=[])。

我又重新錄了一次內存快照,但是發現內存並沒有下降,有可能是gc還沒有執行,手動點擊timeline的gc按鈕,建議gc執行。2分鐘後再次錄製,內存已經從150M降到44M,接近於初始狀態。泄漏點成功定位!

當然,我只是定位了泄漏點,修復問題還需要定位"clickouter"的使用方,我相信這對於有數的開發團隊來說這就是個小case了。

這次實戰,是在完全陌生的環境下進行的,也順利找到了內存泄漏點。如果是項目開發者,可以在代碼沒壓縮的開發環境下排查會更加容易。


推薦閱讀:

前端初學路線指南
如何在有限時間內儘可能高效率地學習前端?
方正的前端學習路線
組件庫設計實戰 - 組件分類、文檔管理與打包發布
工作除了擼代碼,你還幹了什麼?學習是不是你工作的責任?

TAG:JavaScript | 内存管理 | 前端开发 |