前端內存泄露淺析

手上負責的vue項目最近出現一個這樣的問題,用戶用着用着就出現:」喔唷,崩潰啦!「的提示。 javascript

作了如下性能優化嘗試:前端

  • 主動銷燬對象及其子對象
  • 主動取消監聽listener
  • 本地搜索減小組件DOM渲染

主動銷燬對象及其子對象

vue-cropper.js,組件實例不會主動銷燬,須要主動調用destroy方法銷燬。 createjs/easeljs,組件實例須要手動銷燬canvas畫布,maker.stage.canvas = null;maker.stage.removeAllChildren();vue

主動取消監聽listener

createjs/easeljs,maker.stage._eventListeners = null;maker.stage.removeAllEventListeners();java

本地搜索減小組件DOM渲染

iview的select組件,當數據量過大時,DOM渲染會佔用很大的內存,很是吃性能。所以爲其增長了渲染指定個數的功能,例如首次渲染只渲染20個,以後的搜索從已經加載好的數據中搜索並渲染。git

有必定的收效,可是在仍然存在性能問題,切換菜單的過程當中,Memory中的Javascript VM instance以100MB/次的速度增長,並且仍是在沒有數據的狀況下。github

所以,迫切的須要一次深度的性能優化,以解決當前項目遇到的問題。web

解決完這個問題我將加強技能:chrome

  • Chrome DevTools的Memory,Performance工具的應用
  • vue相關,javascript相關,DOM相關的未知內存泄漏知識點

我將記錄如下深度分析內存泄露的相關內容:element-ui

  • 內存泄露分析Snapshot相關知識點
  • 內存泄露分析Snapshot的疑惑和實踐
  • Chrome DevTools Elements的Event Listeners分析內存泄露

內存泄露分析Snapshot相關知識點

JS heap size

window.performance.memory對象的屬性。canvas

jsHeapSizeLimit: 2197815296
totalJSHeapSize: 12068848
usedJSHeapSize: 10730032
複製代碼

totalJSHeapSize和usedJSHeapSize的區別是什麼? usedJsHeapSize是內存總數:指的是JS對象佔用的內存,包括V8內部對象。 totalJsHeapSize是當前內存總數:指的是JS堆的佔用的內存,包括任意js對象的空閒內存

經過如下代碼,能夠觀察當前document的usedJSHeapSize佔用情況,從而分析是否存在內存泄露性能問題。

setInterval(()=>{
	console.log(performance.memory);
},2000)
複製代碼

image

經過觀察能夠發現,js佔用內存(不包括空閒內存)在一直升高,停留一段時間之後也GC不到頁面初始化的的大小。 所以能夠得出結論,存在內存泄露。

也能夠在Chrome的任務管理器中,開啓JavaScript使用的內存的監控。可是這樣會開啓看到全部tab甚至是插件的內存佔用信息,不如code的方式直觀和geek。

Heap snapshot

堆快照。其實就是當前頁面的js對象及其相關的DOM節點的內存分佈狀況。

  • 內存未泄露堆快照
  • 內存泄露堆快照

能夠在內存泄露前生成一份堆快照,再在內存泄露後生成一份堆快照。經過對比的方式,找出兩份堆快照存在的內存泄露點。最好是在一次操做後分析,以便分析出問題。

Shallow Size

Shallow Size 是對象自己hold的內存。 js會爲對象自身開闢一些空間用來存儲數據。js中string和array會有明顯的shallow size, 不過它們主要在渲染內存中存儲,在js heap上僅僅暴露一個包裹對象。 渲染內存指的是監測頁面的全部內存:

  • 原生內存(native memory)
  • 頁面的js堆內存(js Heap memory)
  • 頁面開啓的全部worker的js堆內存(JS heap memory of all dedicated workers)

參考資料:即便是一個小對象,均可能間接的hold了龐大的內存。從而致使自動GC程序不能處理掉這些被間接hold的內存。

Retained Size

這是刪除了對象及其依賴對象後,能夠釋放的內存大小,這些依賴從GC root是沒法訪問到的。

官方解釋很拗口,簡單理解其實就是對象及其依賴對象的內存大小

Comparison中的分析字段

  • # New 新建立的對象個數。
  • # Deleted 刪除的對象個數。
  • # Delta 發生變化的所有對象的個數。淨增對象個數。
  • Alloc.Size 已經分配的使用中的內存空間。
  • Freed Size 新對象釋放出的內存空間。
  • Size Delta 發生變化的釋放內存的所有空間。淨增內存空間。

Heap Snapshot中的Constructor

  • (closure) 經過函數閉包對一組對象的引用計數
  • (array、string、number、regexp) 不一樣對象類型的列表,Array,String,Number,RexExp的屬性
  • (已編譯代碼) 與已編譯代碼相關的任何內容。
  • HTMLDivElement、HTMLAnchorElement、DocumentFragment DOM對象。
  • Dep、Observer、VNode、Watcher、VueComponent 這些是vue特有的對象。

一個構造函數的屬性

  • code :: (CompileLazy builtin) V8的builtins
  • context :: system/NativeContext V8的heap/factory.cc
  • feedback_cell::system V8的heap/factory.cc
  • map::system/Map V8的heap/factory.cc
  • shared [V8的heap/factory.cc]指的是SharedFunctionInfos 這是一個介於函數和已編譯代碼的對象,SFI沒有上下文。
    • function_data 函數數據
    • name_or_scope_info 函數名稱和做用域信息
    • script_or_debug_info 腳本或者debug信息

Shallow size、Retained size、Freed size、Delta size的size是以什麼爲單位?

全部的size都是以字節爲單位的。

Note: Both the Shallow and Retained size columns represent data in bytes.

內存泄露分析Snapshot的疑惑和實踐

爲何一次菜單切換會致使6MB的內存泄露?

image

image

素材列表->產品列表->素材列表,增長了6MB的內存佔用。 通過對比發現,主要增大的是Object的Retained Size,從26913個(37%)增大到32933個(49%),增大了12%。

恰好VueComponent也從377個(10%)增大到600個(22%),也是從增大了12%。

因此初步判定,是因爲VueComponent沒有GC致使的。

第一組疑問(理論):

  • 是有對象沒有被銷燬嗎?
  • 是對象銷燬了可是因爲其餘對象依賴它,致使銷燬失敗嗎?
  • 是對象銷燬了可是因爲其餘對象依賴它的子對象,致使銷燬失敗嗎?

以上信息是在Summary中展現的,那麼如何對比兩次快照呢? Chrome DevTools提供了一個很是便利的功能,Comparison,切換到想要對比的Snapshot,便可獲得2次內存佔用的diff。

通過第二次和第一次的對比,咱們獲得這張對比分析圖。

image

第二組疑問(實踐): 組件做爲實例的組件,不會跟隨父組件自動銷燬嗎? 是否是通用組件的問題?一個通用組件在多處引用,致使頁面銷燬後,當前實例的組件沒有完全銷燬?

#Delta值最高的(closure)是主要的緣由嗎?

image

在(closure)的末尾,咱們找到很熟悉的通用組件面孔,以此爲出發點去作分析。

image

分析 ./src/components/uploadToOss組件

image
shared是很可疑的,點開之後是下圖的場景。

組件在這裏出現,說明這個模塊/組件閉包內部變量使用完後沒有置爲null

vue並不會監測到組件/模塊再也不使用,因此咱們須要在vue的destroyed或者beforeDestroy生命週期中作主動銷燬。

<script>
import ALIOSS from '@/components/uploadToOss';
let commonOSS = new ALIOSS();

export default {
    beforeDestroy() {
        commonOSS = null; // 這是新增的代碼,銷燬建立的上傳OSS組件實例,釋放閉包空間
    }
}
</script>
複製代碼

必定要注意,vue是監測不到咱們不用某些模塊的,只有綁定在vue實例上的實例纔會與組件一塊兒銷燬,沒有綁定的必定要主動銷燬。

置爲null前

image

置爲null後

image

咱們成功釋放了112byte,也就是0.112Kb的內存!

  • 是有對象沒有被銷燬嗎?是的,引入的模塊沒有被銷燬。
  • 是對象銷燬了可是因爲其餘對象依賴它,致使銷燬失敗嗎?不是,咱們暴露的通常是一個class,新建的實例有本身的上下文,不存在單文件組件間互相引用,所以是獨立的。
  • 是對象銷燬了可是因爲其餘對象依賴它的子對象,致使銷燬失敗嗎?對象銷燬後其子對象也會自動銷燬。
  • 組件做爲實例的組件,不會跟隨父組件自動銷燬嗎?會銷燬的。每次引入都是獨立的。
  • 是否是通用組件的問題?一個通用組件在多處引用,致使頁面銷燬後,當前實例的組件沒有完全銷燬?不是。但不是因爲多初引入致使的,而是因爲沒有主動將組件建立的閉包變量置null致使的。

此次分析給了咱們一個啓示呢?在利用class Filter去搜索constructor,觀察delta size是否爲負數,freed size是否不爲0,這樣就能夠判斷出模塊有沒有完全銷燬。

費了半天勁,最後只優化了0.012Kb,這不和沒優化沒差嗎?

試着從VueComponent的對比找找緣由:在產品列表快照,咱們發現了殘留的未被銷燬的素材列表的Table組件。

image
image

因此幾乎能夠肯定的是,切換到素材列表頁面的Table組件,沒有被徹底銷燬,在產品列表中依然能夠找到它的身影。

因此,是iView的Table組件存在內存泄露?仍是vue自己存在內存泄露?

再通過對比element-ui和iView,發現iView確實是存在內存泄漏的,內存佔用一直降不下來,而element-ui過一下子就會降到正常值。因此不是Vue的緣由。

和老大討論了一下,以後可能會替換成其餘的UI框架。

目前的方案是監聽window.performance.memory對象,一段時間內持續大於某個閥值時,會提醒用戶主動刷新頁面,從而釋放出泄露掉的內存。

image

關於iView內存泄露的討論:

個人驗證方式:

  • iView官網幾回切換後停留到同一個頁面,element-ui官網切換 觀察同一個頁面的內存佔用
  • 本地項目幾回切換後停留到同一個頁面,對比VueComponent個數,並找出其餘頁面的組件

image
就拿這個來講,我作了以下的切換foo->bar->foo->baz->foo後,獲取到這個快照對比。

從圖上能夠看出,VueComponent新建了612個,刪除了9個,淨增603個,分配了17.296Kb的內存,釋放了0.504Kb的內存(看到這個釋放程度我真的佛了),淨增16.792Kb的內存。形成了16.792Kb的內存泄露。

可能你以爲16.792Kb不算什麼,由於它在個人此次分析裏,內存泄露狀況只排第19,排名第一第二的分別泄露了598Kb,506Kb。

image

Chrome DevTools Elements的Event Listeners分析內存泄露

vue中的全局事件銷燬,避免listener內存泄露。

DOM0級事件銷燬

window.onbeforeunload = () => {};
window.onbeforeunload = null; // 銷燬,能夠在vue的destroyed生命週期(最好在這個,由於無需在beforeDestroy引用vue實例)或beforeDestroy。
複製代碼

DOM2級事件銷燬

this.foo= (e) => {}
window.addEventListener('resize', this.foo);
window.removeEventListener('resize', this.foo);// 銷燬,能夠在vue的beforeDestroy生命週期(引用vue實例最好在這個週期銷燬)或destroyed。
複製代碼

全局事件銷燬前(內存釋放前):

image
image

全局事件銷燬後(內存釋放後):

image
image

經過觀察能夠發現,一次菜單切換,減小了一個冗餘的全局事件監聽器,性能有些許提高。

總結與思考

通過一系列分析咱們發現,能夠經過如下幾種方式分析內存泄露的問題並修復。

  • 監聽在window的事件沒有解綁
  • 綁在EventBus的事件沒有解綁
  • 第三方庫建立的實例沒有調用銷燬函數
  • 自定義組件/模塊閉包內部變量未被銷燬

前端同窗在選型前端UI框架時,不妨先測試測試是否存在內存泄露。

斯世濁清,全賴吾輩激揚!

參考資料: