VueJS SSR 後端繪製記憶體洩漏的相關解決經驗

再見尼克發表於2018-12-20

引言

Memory Leak 是最難排查除錯的 Bug 種類之一,因為記憶體洩漏是個 undecidable problem,只有開發者才能明確一塊記憶體是不是需要被回收。再加上記憶體洩漏也沒有特定的報錯資訊,只能通過一定時間段的日誌來判斷是否存在記憶體洩漏。大家熟悉的常用除錯工具對排查記憶體洩漏也沒有用武之地。當然了,除了專門用於排查記憶體洩漏的工具(抓取Heap之類的工具)之外。

對於不同的語言,各種排查記憶體洩漏的方式方法也不盡相同。對於 JavaScript 來說,針對不同的平臺,除錯工具也是不一樣的,最常用的恐怕還是 Chrome 自帶的各種利器(針對 browser 也好,nodeJS 也好)都有不錯的使用體驗,網上也有很多使用教程。

這次我想給大家介紹的記憶體洩漏的定位方法,並非工具的使用。而是一些經驗的總結,也就是我所知道的 VueJS SSR 中最容易出現記憶體洩漏的地方,如果大家知道更多 VueJS SSR 記憶體洩漏點,可以在評論處留言告訴更多的人。

難點

遇到過 VueJS SSR 記憶體洩漏的朋友可能知道,針對 VueJS SSR 記憶體洩漏的排查,與普通 NodeJS 和 Browser 平臺相比是要麻煩很多的。如果你使用了 webpack-dev-server 在本地除錯,你會發現常用的記憶體洩漏工具毫無用武之地,因為抓取到的資訊不僅包括 VueJS SSR 程式資訊,還包含了 Webpack 的程式資訊,甚至還有 webpack-dev-server 的各種堆資訊。當然了,你也可以通過各種手段來過濾掉無關的資訊,從而只剩下 VueJS SSR 的堆資訊。

我在排查我們組專案記憶體洩漏的時候,動用了各種常規工具,但最終發現 VueJS SSR 的記憶體洩漏有很大可能性出現在以下地方,也就說如果,你碰巧也有 VueJS SSR 記憶體洩漏的問題,先不要使用記憶體洩漏排查工具,首先從下面幾個地方著手,看看是否有記憶體洩漏的邏輯。可能直擊要害,節約時間。

可能造成洩漏的位置

生命週期處的 beforeCreate/created

以下是 VueJS 開發者看過無數次的說明圖,我還請大家再多看一遍

VueJS SSR 後端繪製記憶體洩漏的相關解決經驗

在官方文件裡,有這麼一句話:

Since there are no dynamic updates, of all the lifecycle hooks, only beforeCreate and created will be called during SSR. This means any code inside other lifecycle hooks such as beforeMount or mounted will only be executed on the client.

也就是說 SSR 跟前端繪製一樣,也有生命週期,只不過 SSR 的生命週期裡只有 beforeCreate 和 created 。

所以你需要首先排查你的元件的 beforeCreate 和 created 裡面是否有記憶體洩漏的程式碼,或者他們是否呼叫了會記憶體洩漏的程式碼。

路由守衛(Route Guards)處

路由也是會引起 SSR 記憶體洩漏的地方之一

跟生命週期不同,所有的 route guard 都會在 SSR 執行。他們分別都是

  • beforeEach
  • beforeRouteUpdate
  • beforeEnter
  • beforeRouteEnter
  • beforeResolve
  • afterEach
  • beforeRouteEnter

Data-Prefetch 處

還需要特別注意的地方就是 Date-prefetch 的地方,裡面很容易出現記憶體洩漏的程式碼。 所謂 Date-prefetch 就是自定義實現的,在SSR處提前獲取第三方資料,用於繪製的過程。

Global Mixin 處

這個記憶體洩漏的點想必大家都已經熟知,作者也在github上詳細闡述過:GitHub issue

簡單來說,就是 global mixin 會給每個 Vue 例項一個拷貝,而不是引用。

記憶體洩漏的例子

以上列舉了一些可能出現記憶體洩漏的地方,那麼具體怎麼樣的程式碼才會引起記憶體洩漏呢?引起程式碼洩漏的例子網上有很多,我在這裡想給大家介紹幾種常見的洩漏例子。

不小心造成的全域性變數

function foo(arg) {
    bar = "this is a hidden global variable";
}
複製程式碼

以上的程式碼會順利執行,但是因為不小心宣告瞭一個 bar 的變數。相當於:

function foo(arg) {
    window.bar = "this is an explicit global variable";
}
複製程式碼

生成了一個全域性變數 window.bar

如果不手動回收,這個全域性變數會一直存在於記憶體中,不會被CG回收。積少成多,最後造成記憶體洩漏。

現在大家都是在各種模組化(CommonJS/AMD/CMD/etc..)之後的環境下進行開發,這種全域性變數的記憶體洩漏的問題基本上是被消除了。但是要提醒大家,由於JavaScript的各種特性,會有很多意想不到的狀況發生。當摸不清頭腦的時候,可以嘗試從這些特性出發找到問題。

被遺忘了的 Timer 或者 callback

請大家先看以下的例子

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // Do stuff with node and someResource.
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);
複製程式碼

乍一看沒啥問題,之後如果 Node 節點從DOM上被移除,因為上面的 callback 對 Node 節點有引用,所以 Node 節點會一直常駐記憶體,不會被CG回收。

要避免以上問題,就要養成 removeEventListenerclearInterval 的習慣。

var someResource = getData();
var interval = setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // Do stuff with node and someResource.
        node.innerHTML = JSON.stringify(someResource));
    } else {
        // Remove Timer
        clearInterval(interval);
    }
}, 1000);
複製程式碼

還比如:

var element = document.getElementById('button');

function onClick(event) {
    element.innerHtml = 'text';
}

element.addEventListener('click', onClick);
// Do stuff
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers that don't
// handle cycles well.
複製程式碼

addEventListener 之後已經要記得 removeEventListener

閉包

閉包造成記憶體洩漏的情況比較複雜,而且較難查詢。限於本文主旨,不做原理說明。

但是,在這裡我給大家推薦一篇非常不錯的文章,詳細地介紹了閉包是如何造成記憶體洩漏的過程:An interesting kind of JavaScript memory leak

總結

個人認為 VueJS SSR 後端繪製記憶體洩漏造成影響要比普通的 VueJS 前端記憶體洩漏造成的影響要更大。

前端記憶體洩漏的影響,都是發生在客戶機器上,而且基本上現代瀏覽器也會做好保護機制,一般自行重新整理之後都會解決。但是,一旦後端繪製記憶體洩漏造成當機之後,整個伺服器都會受影響,危險性更大,搞不好年終獎就沒了。

前端工程師一般都是關注於瀏覽器端表現,在開發過程中的記憶體洩漏問題不太在意也不太容易被發現。一般都是在專案上線一段時間之後,才發現記憶體洩漏的情況。那個時候再去著手,可能會有些無從下手或者手忙腳亂。

那麼,就讓我們在開發的時候開始關注記憶體洩漏問題,將 VueJS SSR 後端繪製記憶體洩漏問題扼殺於襁褓之中。

筆者部落格

相關文章