[訂閱會員專屬文章分享]一個用於尋找 Web 應用程式中的記憶體洩漏工具

在 Web 應用程式中除錯記憶體洩漏是很難的。工具是存在的,但是它很複雜,很麻煩,而且往往不能回答簡單的問題。為什麼我的應用程式會洩漏記憶體?

Introducing fuite: a tool for finding memory leaks in web apps | Read the Tea Leaves (nolanlawson.com)

正因為如此,我敢打賭,大多數網路開發人員都沒有積極監測記憶體洩漏。當然,如果你不對某樣東西進行測試,就很容易出現漏洞。

當我第一次開始研究記憶體洩漏時,我認為這是一件罕見的事情。JavaScript–一種具有自動垃圾收集器的語言–怎麼可能是記憶體洩露的一大來源?但我瞭解得越多,我就越懷疑記憶體洩露在單頁應用程式(SPA)中其實是很常見的–只是沒有人對它進行測試而已!”。

由於大多數網頁開發人員並不是為了好玩而擺弄 Chrome 瀏覽器的記憶體工具,他們可能不會注意到洩漏,直到瀏覽器標籤出現Out Of Memory錯誤而崩潰,或者頁面變慢,或者有人碰巧開啟任務管理器,發現一個網站正在使用許多兆位元組(甚至是千兆位元組!)的記憶體。但在這一點上,情況已經變得足夠糟糕,在同一個頁面上可能有多個洩漏。

我過去曾寫過關於記憶體洩露的文章,但我的建議基本上歸結為 “使用Chrome開發工具,遵循這十幾個繁瑣的步驟,然後也許你能找出你的頁面洩漏的原因。” 這不是一個很好的開發者體驗,我相信很多讀者只是絕望地搖搖頭,然後繼續前進。如果一個工具能自動找到記憶體洩漏,那就更好了

這就是為什麼我寫了fuite(法語中的 “洩漏”)。fuite是一個CLI工具,你可以指向任何URL,它將分析頁面的記憶體洩漏。

npx fuite https://example.com

這就是了! 預設情況下,它假定該網站是一個客戶端渲染的SPA,它將抓取頁面的內部連結(如/關於或/聯絡)。然後,對於每個連結,它都會執行以下步驟。

  • 點選該連結
  • 按下瀏覽器的返回按鈕
  • 重複進行,看看記憶體是否增長了

如果fuite發現任何洩漏,它將顯示哪些物件被懷疑導致洩漏。

Test         : Go to /foo and back
Memory change: +10 MB
Leak detected: Yes
 
Leaking objects:
 
| Object            | # added | Retained size increase |
| ----------------- | ------- | ---------------------- |
| HTMLIFrameElement | 1       | +10 MB                 |
 
Leaking event listeners:
 
| Event        | # added | Nodes  |
| ------------ | ------- | ------ |
| beforeunload | 2       | Window |
 
Leaking DOM nodes:
 
DOM size grew by 6 node(s) 

為了做到這一點,fuite使用了我的部落格中概述的基本策略。它將啟動 Chrome 瀏覽器,執行一些場景 n 次(預設為7次),看看是否有任何物件洩露了 n 次的倍數(7,14,21,等等)。

fuite 還將分析任何陣列、物件、Maps、集合、事件監聽器和整個DOM,看其中是否有洩漏的情況。例如,如果一個陣列在7次迭代後正好增長了7,那麼它就可能是洩露了。

測試真實世界的網站

有點令人驚訝的是,點選內部連結和按下返回按鈕的 “基本 “場景足以在許多SPA中發現記憶體洩漏。我在10個流行的前端框架的主頁上測試了fuite,發現所有這些網站都有洩漏。

SiteLeak detected內部連結平均成長最大成長
Site 1yes827.2 kB43 kB
Site 2yes1050.4 kB78.9 kB
Site 3yes2798.8 kB135 kB
Site 4yes8180 kB212 kB
Site 5yes13266 kB1.07 MB
Site 6yes8638 kB1.15 MB
Site 7yes71.37 MB2.25 MB
Site 8yes153.49 MB4.28 MB
Site 9yes435.57 MB7.37 MB
Site 10yes1614.9 MB186 MB

在這種情況下,”內部連結 “指的是測試的內部連結數量,”平均成長 “指的是每個連結的平均記憶體成長(即點選它然後按下返回按鈕),而 “最大成長 “指的是哪一個內部連結的洩漏量最大。請注意,這些數字不包括一次性的設定成本,因為fuite在正常的7次迭代之前會進行一次預檢迭代。

要想自己確認這些結果,你可以使用Chrome DevTools的記憶體標籤。下面是我這一組中表現最差的網站的截圖,我點選一個連結,按下返回按鈕,拍下一個堆快照,然後重複。

為了避免點名和羞辱,我沒有列出實際的網站。重點是展示一些流行的SPA的代表性樣本–這些網站的作者可以自由地自己執行 fuite 並追蹤這些洩漏。(請這樣做!)。

注意事項

但請注意,並不是 SPA 中的每一個洩漏都是需要解決的惡劣問題。例如,SPA 需要維護焦點和滾動狀態,以正確支援可訪問性,這意味著可能有一些小的元資料被儲存在每個頁面導航中。Fuite會盡職地報告這種洩漏(因為它們是洩漏),但這取決於開發者決定一個微小的洩漏是否值得追逐。

一些記憶體的成長也可能是由於瀏覽器內部的變化(比如JITing),而網頁並不能真正控制這些變化。因此,記憶體成長的數字並不能完美地衡量你透過修復洩漏所獲得的收益–很有可能幾千位元組的成長是不可避免的。(儘管 fuite 試圖忽略瀏覽器內部的成長,而且只有在對網頁開發者有可操作的建議時才會說 “檢測到洩漏”)。

在極少數情況下,一些記憶體成長也可能是由於直接的瀏覽器bug造成的。在分析上面的網站時,我發現有一個網站(4號網站)似乎由於 <img loading=”lazy”> 沒有被解除安裝而受到 Chrome 瀏覽器 bug 的影響。不幸的是,Fuite很難檢測到瀏覽器的bug,所以如果你對一個漏洞感到神秘,最好是與其他瀏覽器進行交叉檢查

還要注意的是,多頁面應用程式(MPA)幾乎不可能發生洩漏因為瀏覽器會在每個頁面導航時清除記憶體。(在我的測試中,我發現有兩個前端框架的主頁是MPA,毫不奇怪,fuite在它們身上找不到任何洩漏。這些被排除在上面的結果之外。

記憶體洩漏對SPA來說是一個更大的問題,因為在每次導航時記憶體不會被自動清除

fuite 目前只測量頁面主框架中的 JavaScript Heap 記憶體,所以跨源iframe、Web Worker和Service Workers不被測量。像 performance.measureUserAgentSpecificMemory() 這樣的東西會更準確,但它只在 cross-origin isolated 情況下可用,所以現在對於一個通用工具來說並不實用。

其他記憶體洩漏的情況


fuite 是建立在 Puppeteer 之上的,所以對於你想測試的任何情境,你基本上只需要寫一個 Puppeteer 指令碼來告訴瀏覽器該怎麼做。你可能會測試的一些常見場景是。

  • 開啟一個 modal 的對話方塊,然後關閉它
  • 將滑鼠懸停在一個元素上,顯示一個工具提示,然後將滑鼠移開,使其失效
  • 滾動瀏覽一個無限載入的列表,然後離開並返回
  • 等等。

在每一種情況下,你都會期望記憶體在之前和之後都是一樣的。但當然,對於 Web 應用程式來說,這並不總是那麼簡單 你可能會驚訝於你的對話方塊和工具提示中有多少記憶體洩漏

為了分析洩漏,fuite 捕捉了  heap snapshot files,你可以在 Chrome DevTools 中載入這些檔案來檢查。它還有一個–除錯模式,你可以用它來進行更精細的分析:在測試過程中逐步進行測試,即時除錯瀏覽器,分析洩漏的物件等等。

在引擎蓋下,fuite 是一個相當基本的工具,我不會宣稱它可以完成修復記憶體洩露的100%的工作。仍然存在著人為的成分,即弄清楚為什麼你的物件被分配和保留,然後找到一個合理的修復方法。但我的目標是將 95% 的工作自動化,這樣一來,修復 Web 應用程式中的記憶體洩露實際上就可以實現。

你可以在 GitHub 上找到fuite。尋漏快樂

更新:我做了一個影片教程,展示如何用fuite除錯記憶體洩露。

Comments are closed.

由 WordPress.com 建置.

Up ↑

探索更多來自 Soft & Share 的內容

立即訂閱即可持續閱讀,還能取得所有封存文章。

Continue reading