前幾天項目組一個app內嵌h5頁面在長時間使用後(半個小時),就會出現頁面卡頓的狀況。頁面內元素的點擊事件須要等待5秒多才能進行響應,在排除了原生app形成問題的狀況以後我基本判斷應該是前端本身的問題——內存泄漏。html
前幾天組內一個前端妹子火急火燎的跑了過來講,她所負責的一個app內嵌h5項目首頁在持續使用了一段時間以後會變得異常卡頓。一個簡單的頁面跳轉或者點擊行爲都會卡5秒多才會進行響應。前端
最終通過層層排查確認是內存泄漏形成的問題,出現的bug則是由於對於js中的this指向掌握不充分致使this指向丟失,出現問題的代碼出如今這裏:node
//h5調用原生 經過iframe urlScheme的方法
callNativeByUrlScheme(eventName: string, data: any) {
const url = this.getUrlScheme(eventName, data);
const iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(iframe.remove, 100);
}
複製代碼
在項目封裝的jsBridge中,前端通知原生的交互方式是經過url scheme
的形式來實現的(h5建立一個隱藏的iframe標籤,原生經過劫持url的形式來進行通訊處理)。這是項目中建立iframe所封裝的一個函數,不知道你是否能發生發現這個隱晦的bug呢?android
在通過簡單的排查以後,在發現v-console
也是卡頓56秒以後基本確認是由於內存泄漏問題的形成的卡頓問題了。可是由於咱們項目首頁的邏輯異常沉重,涉及到大量的原生交互事件以及回調,並且也有大量的定時器進行輪詢獲取數據進行處理。**如何快速的定位是否內存泄漏形成的問題?如何快速的定位到是那一行代碼形成的內存泄漏?**就成了如今的難題之一。git
問題的解決辦法:獲取到當前項目的堆棧快照加以分析處理。在nodejs中咱們能夠經過node-heapdump
這個庫來獲取nodejs的堆棧快照文件,在瀏覽器中咱們能夠經過chrome的開發者工具memory
模塊來獲取當前頁面的內存快照信息。github
接下來我會大體講述是如何一步一步解決這個內存泄漏的問題的。web
由於須要調試的h5頁面是app裏內嵌的webview頁面,依賴於native提供的能力,這種狀況在pc上很難模擬因此須要使用真機調試方式。若是是比較獨立的頁面的話能夠放到chrome瀏覽器中打開直接進行調試。面試
由於當前項目是內嵌在app裏面的,涉及到一些加密以及鑑權的流程並且爲了更好的模擬實際環境,咱們決定放棄經過chrome打開頁面而是直接使用真機調試的方式來解決bug。chrome
由於Android webview
已經幫咱們開啓了調試模式,爲了方便直接使用android手機 + 電腦chrome瀏覽器
的組合來方便真機調試。具體的調試方法能夠參考阮一峯老師的這篇博客api
按上面的步驟鏈接完手機以後,爲了確認是否內存泄漏的問題咱們須要打印內存快照
::
Memory
模塊。Head snapshot
模式而且點擊Take heap snapshot
按鈕開始打印第一份快照,這時候獲得了第一份快照Snapshot1
。Collect garbage
手動執行垃圾回收策略,再次點擊Take heap snapshot
按鈕打印第二分內存快照獲得兩份快照以後,咱們發現第二份快照的內存佔用明顯比第一份增長了不少,在手動執行了垃圾回收後佔用的內存依然沒被釋放。至此確認:確實是內存泄漏問題形成的頁面卡頓現象。
爲了百分百確認,咱們打了第三份快照。確認內存佔用依然明顯增長而且通過垃圾回收沒有釋放,至此問題停留在了——如何經過內存快照定位到具體的形成內存泄漏的代碼。
獲取到了兩分內存快照以後,chrome的memory
模塊提供了不一樣的快照類型查閱模式:
在這裏,咱們使用Comparison
模式,以快照2爲基準對比快照1的內存變化差別:
在進行對比中,咱們發現內存增長最明顯的兩塊地方system
以及closure
。
system
欄目看不出什麼有效的信息,所以咱們展開closure
欄目,咱們不難發現內存中增長了許多document
元素節點,可是通常來講一個html頁面通常來講應該只會出現一個document
節點。
忽然間臨機一動,經過iframe
標籤能夠將另外一個HTML頁面嵌入到當前頁面中,這樣子就會在iframe上下文中也會建立對應的document節點。而後又聯想到在和原生端進行通訊的過程當中咱們是經過建立iframe的形式來進行通訊的。猜想多是用於原生通訊的iframe標籤在建立完成以後沒有被清除仍然停留在dom樹中致使沒法被垃圾回收。
爲了驗證上面的猜測,咱們只須要在chrome的控制檯中獲取dom樹中的iframe標籤到底有多少個就能夠了:
$$('iframe')
複製代碼
全局搜索找到相關的建立iframe
的代碼,最終定位到這一個函數上:
callNativeByUrlScheme(eventName: string, data: any) {
const url = this.getUrlScheme(eventName, data);
const iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(iframe.remove, 100);
}
複製代碼
從上可知,內存泄漏的緣由出現於:用於跟原生通訊交互而建立的iframe標籤並無從dom樹中刪除致使沒法被v8進行垃圾回收。
回到這行代碼上,iframe
的節點上是經過iframe.remove
方法進行刪除的。這個方法在MDN上確認是沒問題的:ChildNode.remove()
方法,把對象從它所屬的 DOM 樹中刪除。
callNativeByUrlScheme(eventName: string, data: any) {
const url = this.getUrlScheme(eventName, data);
const iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(iframe.remove, 100);
}
複製代碼
確認api的使用是確實沒有問題以後,咱們把焦點彙集在了setTimeout
上,是否是由於異步的問題致使remove
方法調用失敗, 因而乎咱們嘗試使用同步的方式來調用remove
方法。
// 改動代碼
document.body.appendChild(iframe);
//setTimeout(iframe.remove, 100);
// 改用同步的方式
iframe.remove()
複製代碼
改用同步以後從新打包。。。問題解決了!也沒有再出現內存泄漏的問題,dom樹中建立的iframe
節點每次建立成功也被及時的清理了。WTF???? 難道ChildNode.remove()
不方法不支持異步調用?
思考了一段時間,而後把目光聚焦在了變化的兩端代碼上發現了除了異步以外的另一個差一點:setTimeout中傳入的是remove的函數引用,在setTimeout中本質上是函數自調用,而同步的寫法則是對象語法調用
// 函數自調用
setTimeout(iframe.remove, 100);
// setTimeout大體模擬實現
//async function setTimeout(fn,delay) {
// await delay()
// fn &&fn()
//}
// 對象語法調用
iframe.remove()
複製代碼
熟悉this指向的同窗們會清楚,在js語法中this的指向大概能夠分爲四種:
構造函數綁定(new)
:綁定到新建立的對象,注意:顯示return函數或對象,返回值不是新建立的對象,而是顯式返回的函數或對象。顯示綁定(call,apply,bind)
:嚴格模式下,綁定到指定的第一個參數。非嚴格模式下,null和undefined,指向全局對象(瀏覽器中是window),其他值指向被new Object()包裝的對象。隱性綁定(對象上的函數調用)
: 綁定到那個對象。默認綁定(函數自調用)
:在嚴格模式下綁定到 undefined,不然綁定到全局對象。setTimeout中傳入的是remove的函數引用,在setTimeout中本質上是函數自調用,因此remove方法中的this指向的是undefined或者全局對象
iframe.remove()是對象調用因此this指向的是iframe元素自己
爲此我找到MDN上的ChildNode.remove()
profilly代碼,確認了內部實現中確實使用到了this變量來指向元素節點:
//https://github.com/jserz/js_piece/blob/master/DOM/ChildNode/remove()/remove().md
(function (arr) {
arr.forEach(function (item) {
if (item.hasOwnProperty('remove')) {
return;
}
Object.defineProperty(item, 'remove', {
configurable: true,
enumerable: true,
writable: true,
// 注意這裏
value: function remove() {
this.parentNode.removeChild(this);
}
});
});
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
複製代碼
最終咱們肯定了內存泄漏發生的真正緣由:this指向丟失形成建立的iframe元素沒法從dom樹中刪除致使的內存泄漏。
在確認了問題的緣由以後,解決問題的辦法就容易多了:
iframe.remove()
或者setTimeout(() => iframe.remove(), 100);
setTimeout(iframe.remove.bind(iframe), 100);
至此,一次從發現問題——查找問題——解決問題的鏈路式解決內存泄漏的旅程結束了。歸根到底,出現這個bug的主要緣由有兩個:
內存泄漏出現不可怕,如何解決如何定位到問題的緣由纔可怕。感謝各位小夥伴們的查閱,但願這篇文章能給遇到內存泄漏困擾的同窗們提供幫助啦!