瘋狂的技術宅 前端先鋒 前端
翻譯:瘋狂的技術宅
做者:Nolan Lawson
來源:nolanlawson
正文共:4737 字
預計閱讀時間:10 分鐘git
從服務器端渲染的網站切換到客戶端渲染的 SPA 時,咱們忽然不得不更加註意用戶設備上的資源,必須作不少工做:不要阻塞 UI 線程,不要使筆記本電腦的風扇瘋狂旋轉,不要耗盡手機的電池等。咱們將交互性和「類應用程序」行爲轉換成了更好的新型問題,這些問題實際上並不存在在服務端渲染的世界中。github
這些問題中最主要的一個是內存泄漏。編碼不正確的 SPA 可能很容易耗盡 MB 甚至 GB 的內存,從而繼續吞噬愈來愈多的資源,即便它無辜地存在於後臺標籤中也是如此。這時頁面可能開始變成龜速,或者瀏覽器終止了標籤頁,你將會看到熟悉的 「Aw, snap!」 頁面。
Chrome page saying "Aw snap! Something went wrong while displaying this web page."
(固然,服務端渲染的網站也可能會泄漏服務器端的內存。可是客戶端泄漏內存的可能性很小,由於每次你在頁面之間導航時瀏覽器都會清除內存。)web
Web 開發文獻中沒有很好地解決內存泄漏問題的方法。可是,我很是肯定大多數不凡的 SPA 都會泄漏內存,除非它們背後的團隊擁有強大的基礎結構來捕獲和修復內存泄漏。用 JavaScript 太容易了,以致於不當心分配了一些內存而忘了清理它。編程
那麼,爲何關於內存泄漏的文章這麼少呢?個人猜想是:數組
像 React、Vue 和 Svelte 這樣的現代 Web 框架都使用基於組件的模型。在此模型中,產生內存泄漏的最多見方法是這樣的:瀏覽器
1window.addEventListener('message', this.onMessage.bind(this));
就這樣,引入了一個內存泄漏。若是你在某些全局對象(window、<body> 等)上調用 addEventListener 而後在卸載組件時忘記用 removeEventListener 進行清理,就會產生一個內存泄漏。緩存
更糟糕的是,你剛剛泄漏了整個組件。因爲 this.onMessage 綁定到 this,因此組件已泄漏,包括其全部子組件。並且極可能全部與組件相關聯的 DOM 節點也是如此。這會很快會變得很是糟糕。安全
解決方法是:服務器
1// Mount phase 2this.onMessage = this.onMessage.bind(this); 3window.addEventListener('message', this.onMessage); 4 5// Unmount phase 6window.removeEventListener('message', this.onMessage);
注意,咱們保存了對綁定的 onMessage 函數的引用。你必須把前面傳給 addEventListener 的函數再原封不動的傳給 removeEventListener,不然它將沒法正常工做。
以個人經驗,最多見的內存泄漏源與如下 API 相關:
這是困難的部分。首先我要說的是,我認爲那裏的任何工具都不是很好。我嘗試使用 Firefox 的內存工具,Edge 和 IE 內存工具,甚至 Windows Performance Analyzer。同類最佳的仍然是 Chrome Dev Tools,可是它有不少雜亂的細節值得咱們瞭解。
在 Chrome Dev Tools中,咱們選擇的主要工具是「內存(Memory)」標籤中的「堆快照(heap snapshot)」。Chrome 中還有其餘存儲工具,但我發現它們對識別泄漏不是頗有幫助。
帶有堆快照工具的Chrome DevTools內存選項卡
堆快照工具使你能夠捕獲主線程、Web Worker 或 iframe 的內存。
當你點擊「獲取快照(take snapshot)」按鈕時,你已經捕獲了該網頁上特定 JavaScript VM 中的全部活動對象。這包括 window 所引用的對象,setInterval 回調所引用的對象等。可將其視爲時間暫停後,表明該網頁使用的全部內存。
下一步是重現你認爲可能正在泄漏的某些場景,例如,打開和關閉模態對話框。對話框關閉後,你但願內存恢復到上一級。所以,你獲取了另外一個快照,而後將其與上一個快照進行比較。這種差別確實是該工具的殺手級特性。
顯示第一個堆快照的示意圖,而後是一個泄漏的場景,而後是第二個堆快照,該快照應該等於第一個
可是,你應該注意該工具的一些限制:
我發現消除噪音的最好方法是屢次重複泄漏狀況。例如,你不只能夠執行一次打開和關閉模式對話框這種操做,還能夠將其打開和關閉 7 次。(7 是一個質數。)而後你能夠檢查堆快照 diff,以查看是否有什麼對象泄漏7次。(或14次或21次。)
Chrome開發者工具堆快照差別的截圖顯示了六個堆快照捕獲,其中有多個對象泄漏了7次
堆快照差別。請注意,咱們正在將 6 號快照與 3 號快照進行比較,由於我連續拍攝了三個快照,以便進行更多的垃圾收集。注意,有幾個對象泄漏了 7 次。
(另外一種有用的技術是在記錄第一個快照以前對方案進行一次遍歷。特別是若是你進行大量的代碼拆分,則方案可能會花費一次內存來加載必要的 JavaScript 模塊。)
你可能想知道爲何應該按對象數而不是總內存進行排序。直觀地講,咱們正在努力減小內存泄漏的數量,因此咱們不該該專一於總的內存使用狀況嗎?嗯,這不是很好,有一個很重要的緣由。
當什麼東西泄漏時,是由於你想要獲得香蕉,可是最終獲得的是香蕉、拿着香蕉的大猩猩以及整個叢林。若是你基於總字節數進行衡量,那麼你所衡量的是叢林,而不是香蕉。
大猩猩吃香蕉
讓咱們回到上面的 addEventListener 的例子。泄漏的來源是事件偵聽器,該事件偵聽器引用一個函數,該函數引用一個組件,該組件可能引用大量的東西,例如數組、字符串和對象。
若是你按總內存對堆快照差別進行排序,那麼它將向你顯示一堆數組、字符串和對象——其中大多數可能與泄漏無關。你真正想要找到的是事件偵聽器,可是與它所引用的內容相比,佔用的內存很小。要修復泄漏,你要找到香蕉,而不是叢林。
因此,若是按泄漏對象的數量進行排序,則會看到 7 個事件監聽器。多是 7 個組件和 14 個子組件等等。「7」 應該像腰間盤同樣突出,由於它是一個不尋常的數字。並且,不管你重複該場景多少次,都應該確切的看到泄漏的對象數量。這樣能夠快速找到泄漏源。
堆快照差別還將向你顯示一個 「retainer」 鏈,該鏈顯示哪些對象指向哪些其餘對象,從而使內存保持活動狀態。這樣能夠弄清楚泄漏對象的分配位置。
事件監聽器引用的閉包所引用的 someObject 的 retainer 鏈
retainer 鏈將向你顯示哪一個對象正在引用泄漏的對象。讀取它的方式是每一個對象都由其下面的對象引用。
在上面的示例中,有一個名爲 someObject 的變量,該變量由閉包(也稱爲「上下文」)引用,並由事件偵聽器引用。若是單擊源連接,它將帶你到 JavaScript 聲明,這很簡單:
1class SomeObject () { /* ... */ } 2 3const someObject = new SomeObject(); 4const onMessage = () => { /* ... */ }; 5window.addEventListener('message', onMessage);
在上面的示例中,「上下文」是 onMessage 閉包,它引用了 someObject 變量。(這是一我的爲的例子(https://github.com/nolanlawson/pinafore/commit/de6ca2d85334ad5f657ddd0f335750b60afab895);實際的內存泄漏可能不那麼明顯!)
可是堆快照工具備幾個限制:
可是,本指南只是一個開始——除此以外,你還必須隨手設置斷點、記錄日誌並測試你的修復程序,以查看它是否能夠解決泄漏。不幸的是,這是一個很是耗時的過程。
在此以前,我要說的是,我尚未找到一種自動檢測內存泄漏的好方法。Chrome 有非標準的 performance.memory API,但出於隱私方面的考慮它沒有很是精確的粒度(https://bugs.webkit.org/show_bug.cgi?id=80444),所以你不能真正在生產中用它來識別泄漏。W3C 網絡性能工做組過去討論了內存 工具,但還沒有就取代該 API 的新標準達成共識。
在實驗室或綜合測試環境中,你能夠用 Chrome 標誌 --enable-precise-memory-info。還能夠經過調用專有的 Chromedriver 命令 :takeHeapSnapshot 建立堆快照文件。可是這也具備上述相同的限制——你可能想要連續獲取三個並丟棄前兩個。
因爲事件監聽器是最多見的內存泄漏源,所以我使用的另外一種技術是對 monkey-patch 的 addEventListener 和 removeEventListener API進行計數,從而進行計數引用並確保它們返回零。這裏是如何執行此操做的示例(https://github.com/nolanlawson/pinafore/blob/2edbd4746dfb5a7c894cb8861cf315c800a16393/tests/spyDomListeners.js)。
在 Chrome Dev Tools 中,你還可使用專有的 getEventListeners() API 來查看事件監聽器附加到特定元素。注意,這隻能在 Dev Tools 中使用。
在 Web 應用中查找和修復內存泄漏的狀態仍然很初級。在本文中,我介紹了一些對我有用的技術,可是請記住,這仍然是一個困難且耗時的過程。
與大多數性能問題同樣,少許預防賽過大量的治療。你可能會發現進行綜合測試是值得的,而不是在事實發生後嘗試調試內存泄漏。尤爲是若是頁面上存在多個泄漏,則可能會變成洋蔥剝皮練習——你先修復一個泄漏,而後查找另外一個泄漏,而後重複(整個過程都在哭泣!)。若是你知道要查找的內容,代碼審查還能夠幫助捕獲常見的內存泄漏模式。
JavaScript 是一種內存安全的語言,具備諷刺意味的是,在 Web 應用中泄漏內存有多麼容易。不過部分緣由只是 UI 設計所固有的——咱們須要偵聽鼠標事件、滾動事件、鍵盤事件等,而這些都是容易致使內存泄漏的模式。可是,經過嘗試下降 Web 應用的內存使用量,能夠提升運行時性能,避免崩潰,並尊重用戶設備上的資源限制。
感謝 Jake Archibald 和 Yang Guo 對本文的草稿提供反饋。感謝 Dinko Bajric 發明了「choose a prime number」技術,我發現它在內存泄漏分析中很是有用。
原文連接
https://nolanlawson.com/2020/02/19/fixing-memory-leaks-in-web-applications/