攜程桌面應用的前端內存優化與監控

一、背景

桌面應用的前端場景不同於傳統前端,具有使用者停留時間長,功能複雜且高度聚集在單一頁面等特徵,因此帶來了不同的技術挑戰,其中很重要的一點是內存泄漏問題。

1)什麼是內存泄漏?

內存泄漏[1](Memory leak)是在計算機科學中,由於疏忽或錯誤造成程序未能釋放已經不再使用的內存。內存泄漏並非指內存在物理上的消失,而是應用程序分配某段內存後,由於設計錯誤,導致在釋放該段內存之前就失去了對該段內存的控制,從而造成了內存的浪費。

2)JavaScript的內存管理

像C語言這樣的底層語言一般都有底層的內存管理接口,比如 malloc()和free()。相反,JavaScript是在創建變量(對象,字符串等)時自動進行了分配內存,並且在不使用它們時“自動”釋放。釋放的過程稱爲垃圾回收。這個“自動”是混亂的根源,並讓JavaScript(和其他高級語言)開發者錯誤的感覺他們可以不關心內存管理[2]。

3)案例

以攜程的IM+項目爲例:IM+將多種溝通渠道整合於一體,使客服人員能夠全方位地觸達用戶,提供便捷、全面的服務,進而實現優質的用戶體驗。所以,在IM+的主頁面當中,同時聚集了IM、電話和郵件三大塊功能,爲了提升坐席的效率和服務質量,還有衆多輔助信息模塊、回覆超時提示模塊,也就導致主頁面功能非常複雜。

因此,主頁面的功能複雜度、代碼複雜度都很高,在大量需求的快速迭代期間,一些細節點考慮不夠或者某些API使用方式不正確,就會比較容易發生內存泄漏問題。另外,又因爲使用者長時間不關閉應用,一旦發生該問題,將會隨着時間的推移,泄漏的內存量越積越多,最終影響整個電腦的資源使用情況,造成諸如應用崩潰、電腦卡頓等較爲嚴重的後果。

綜上所述,桌面應用的前端開發同學需要額外注意內存的問題,而這個場景在用戶停留時間短、功能不重度集中的傳統前端頁面上基本不存在,所以網絡上鮮有這個問題的處理方法。本文提出了一套完整的解決方案,包括:內存佔用分析、內存的優化與驗證、如何在功能迭代中維持低內存佔用,以及線上的內存使用監控。

二、內存佔用分析

在此提出兩種內存佔用分析方法,分別是使用谷歌瀏覽器的Memory插件分析方法和簡單粗暴的單一變量實驗分析法。

2.1 使用谷歌瀏覽器Memory插件分析內存佔用

打開谷歌瀏覽器的調試頁面,選擇Memory Tab,然後點擊Take snapshot獲取內存快照,執行一段時間頁面操作後,再次Take snapshot,然後對比,可以找到觸發內存泄漏的組件(如下圖)和獨立的dom節點。

使用這個組件的時候,需要注意以下三點:

1)Network的請求、控制檯裏的日誌也會佔用Chrome的內存,所以在測試之前,最好把它們清理掉。

2)由於JavaScript的內存管理在語言之內,所以無法確定在獲取內存快照之前是否有即將被釋放掉的內存,這時可以點擊Memory Tab左上角的垃圾回收按鈕,手動觸發一次垃圾回收,可以確保兩次內存快照中都沒有即將被清除掉的內存佔用。

3)查找detached DOM節點

DOM節點的垃圾回收機制是:當頁面的DOM樹和JavaScript代碼都沒有對某個DOM節點的引用時,纔可以對其進行垃圾回收。如果一個DOM節點已經被從DOM樹中刪除,但某些JavaScript變量仍引用該節點,則該節點被稱爲detached DOM節點,不會被回收。它是內存泄漏的常見原因。

在上圖的Memory插件中,可以使用篩選器,輸入關鍵字“Detached”查找分離的DOM樹,然後點擊DOM可以查看引用它的變量位置。找到之後,可以使用ES6的 WeakSet/WeakMap去解決這個問題。

2.2 二分法查找組件的內存泄漏

上面的方法雖然行之有效,但是對於極其複雜的項目,通過上述方法獲取到的內存快照也極其複雜,比較難讀,有的時候很難找到各個內存泄漏點,或者即便找到了內存泄漏的組件,也不清楚具體泄漏在了組件的哪一個功能點,哪一行代碼上。所以針對這個問題,我們提出了二分法的思路。

首先,針對功能頁面,整理總結出高頻操作的功能列表,轉換成自動化腳本,然後先執行腳本,記錄內存佔用。之後,在不影響主體功能的情況下,把組件分爲兩部分,輪流注釋掉,分別執行腳本,記錄內存佔用。最後,對比兩批組件的內存佔用變化情況,判斷內存泄漏主要集中在哪一批組件裏。以此類推,可以在確定到組件之後,將二分法降級到功能維度,甚至代碼維度,最終找到內存泄漏點。

在實際使用當中,我們綜合這兩種方法,逐步分塊查找,最終解決了內存泄漏的問題。

三、內存優化與驗證

3.1 內存的優化

1)可能導致內存泄漏的寫法

i. 事件監聽未正確移除:採用觀察者模式,在組件內部註冊監聽,或是在一些DOM上註冊事件後,需要在組件卸載生命週期中移除監聽,否則可能造成內存泄漏。

ii. 組件初始化前/銷燬後設置State:組件中存在異步調用,調用完成後觸發狀態設置,但是在調用完成前組件已銷燬,就會產生內存泄漏(控制檯會提示:Can’t perform a React state update on an unmounted component. Thisis a no-op, but it indicates a memory lead in your application.)。解決方案:在組件卸載聲明週期中將setState置爲空函數,或撤銷異步調用。

iii. 組件的引用:比如我們的UI確認組件A 在使用完畢後,要釋放對來自調用方組件B內部回調函數的引用,因爲組件A跟B沒有父子關係,所以使用完畢後如果沒有釋放引用,就會導致組件B不能被銷燬,從而導致內存泄漏。

iv. 高頻刷新功能集成在大組件中:一些高頻刷新的功能,比如說時間顯示,最好寫在小組件裏,不要放出來讓它觸發大組件的刷新,因爲所有的內存泄漏都是積小成多的,如果有內存泄漏,刷新次數越多積攢越多,而大組件因爲功能多邏輯複雜,容易內存泄漏,所以高頻刷新的功能最好單獨寫成小組件。

v. 異常處理:未捕獲的異常會造成內存泄漏,console.error也會。其實很好理解,異常隨便什麼時候開調試頁面都能看到,就是因爲存儲在內存裏了,所以我們要處理好異常邏輯。

2)React的shouldComponentUpdate生命週期和Immutable、PureRender:存在內存泄漏的時候,減少渲染次數也可以降低內存泄漏的影響。所以針對減少渲染次數的問題,在React框架下,可以採用這樣幾種方法:

首先,React的shouldComponentUpdate生命週期暴露了鉤子,允許用戶判斷是否需要重新渲染;然後,Immutable可以支持在數據變化的情況下,基於字典序在新地址上覆用原有的數據,減少內存佔用;最後,PureRender則可以用淺比較自動計算shouldComponentUpdate的結果。

3.2 優化後的驗證

1)通過功能埋點分析整理出主要的高頻功能。

IM+使用了攜程的前端埋點框架,可以分析各個DOM的點擊情況,基於點擊數據和對業務邏輯的理解,可以獲知用戶使用的高頻功能。

2)基於Selenium實現主流程的自動化測試。

四、在功能迭代中維持低內存佔用

1)制定避免內存泄漏的代碼規範,在代碼審覈流程中予以檢驗。

2)每次發佈版本前,長時間循環執行主流程自動化測試,對比測試前後的內存開銷。

五、內存使用線上監控

1)調用系統api獲取IM+進程的內存開銷、總CPU開銷、網絡延遲等。

2)上報內存、CPU等信息,彙總到ES中。

3)在監控面板中,展示內存、CPU的佔用情況。

通過上述優化步驟,IM+桌面應用的內存佔用,從之前的隨着使用時間快速增長,動輒佔用數G,降低到了穩定不變的150M左右。

【引用】

[1]內存泄漏.

https://zh.wikipedia.org/wiki/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F

[2] 內存管理

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management

作者介紹

呂萌萌,攜程資深前端開發工程師,關注前端性能優化與前端框架建設

本文轉載自公衆號攜程技術(ID:ctriptech)。

原文鏈接

攜程桌面應用的前端內存優化與監控

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章