發布日期:2026 年 10 月 29 日
Google Chat 是大型單頁應用程式。使用者經常在瀏覽器分頁中開啟網頁應用程式,並持續數天或數週。在長期執行的應用程式中,用戶端記憶體健康狀態是首要考量。未收集的閉包和分離的 DOM 樹會緩慢累積,最終影響 UI 效能、垃圾收集暫停,或在受限裝置上造成分頁當機。
為解決這個問題,Google Chat 團隊發起一項計畫,找出並修正記憶體問題。他們使用代理專用 Chrome 開發人員工具 (包含開發人員工具 MCP 伺服器),將困難的手動程序轉換為自動化工作流程。
挑戰:大海撈針
如要找出流失物件保留在記憶體中的確切原因,必須透過高度壓縮的程式碼追蹤保留路徑。雖然 Chrome 開發人員工具記憶體面板最近經過改良 (例如進階邊緣篩選和物件查詢),讓這項作業變得更簡單,但手動處理 500 MB 的快照仍可能耗費時間和精力。
修復漏水問題需要兩種不同的知識:
- 您需要瞭解 V8 JavaScript 引擎如何處理已編譯的位元碼。
- 您也需要瞭解應用程式的特定狀態管理和元件生命週期。
如要手動連結這兩組不同的知識,通常需要花費數天調查,並與多位開發人員合作,才能解決每次洩漏問題。
解決方案:使用代理瀏覽堆積
團隊發現,雖然大型語言模型非常擅長推論結構化資料,但仍需要合適的工具來處理大量檔案,同時將權杖用量控制在合理範圍內。他們將 Google Antigravity 連結至已啟用記憶體工具 (--memoryDebugging=true) 的開放原始碼 Chrome 開發人員工具 MCP 伺服器,讓代理能直接透過程式存取 V8 堆積。
代理程式會直接查詢快照,開發人員不必在 Chrome 開發人員工具記憶體面板中手動展開保留樹。團隊使用簡單的提示詞,開始進行這項程序:
Discover memory leaks in the heap snapshot chat.heapsnapshot using Chrome DevTools memory tools.
代理使用記憶體偵錯技能,透過特定 MCP 工具執行結構化工作流程:
- 擷取或載入快照:使用
take_heapsnapshot等工具,或讀取現有檔案。 - 分析堆積結構:使用
get_heapsnapshot_details和compare_heapsnapshots掃描常見的記憶體流失來源。 - 追蹤保留路徑:使用
get_heapsnapshot_retaining_paths沿著支配樹狀結構回溯至垃圾收集根。 - 生成修正程式:交叉比對保留路徑與應用程式原始碼,輸出高度具體的修補程式。
技術深入分析:三種常見的記憶體流失模式
代理程式發現了 30 多個不同的洩漏點。以下是代理找到的三個最有效率的模式,代表現代網頁應用程式中節省最多記憶體和最常見的架構反模式。
靜態錯誤堆疊追蹤陷阱
代理程式發現,最大的單一洩漏與預先分配的錯誤物件有關,這些物件永久保留了近 20 MB 的記憶體。舉例來說,單一靜態錯誤常數 (EMPTY_KEY_ERROR) 本身就保留了 14.1 MB。
問題:在 V8 等現代 JavaScript 引擎中,建立 Error 物件會擷取目前呼叫堆疊的快照,協助偵錯。保留大小之所以如此龐大,正是因為這個機制:
- 錯誤是全域常數:在初始模組評估期間例項化。
- JavaScript 引擎擷取堆疊:當時堆疊上的活動函式是應用程式的主要初始化包裝函式 (立即叫用函式運算式,或 IIFE)。
- 陣列困住記憶體:由於應用程式從未對錯誤進行垃圾收集,其內部堆疊追蹤陣列會永久保留對主套件初始化函式的參照。
// Ineffective pattern: Instantiating Error objects at module evaluation time
export const EMPTY_KEY_ERROR = new Error("Key cannot be empty");
export function validateKey(key: string) {
if (!key) {
throw EMPTY_KEY_ERROR;
}
}
修正方式:團隊變更程式碼,在擲回錯誤時動態建立錯誤。如果確實需要可重複使用的靜態錯誤,請明確清除堆疊 (EMPTY_KEY_ERROR.stack = '';),立即釋出擷取的 V8 內部框架。
// Recommended: Construct errors dynamically
export function validateKey(key: string) {
if (!key) {
throw new Error("Key cannot be empty");
}
}
// Or clear the stack property if a static instance is necessary:
export const REUSABLE_ERROR = new Error("Static error");
REUSABLE_ERROR.stack = ""; // Releases the captured initialization frame
調整全域視窗事件監聽器
代理發現記憶體擴充問題,涉及 visibilitychange 個事件監聽器。雖然這並非嚴格意義上的「外洩」(因為元件卸載時最終會清除),但由於畫面上的每則訊息都有一個監聽器,因此記憶體用量會增加。快照顯示有 1,142 個內建事件監聽器,其中 128 個有效監聽器直接附加至全域視窗物件。
問題:在 JavaScript 中,事件監聽器回呼是閉包。閉包會保留對其封閉詞彙範圍的參照,確保其所需的變數仍可存取。代理程式追蹤到其中 126 個事件監聽器都來自同一個元件:RelativeTimeController。Chat UI 中的每個時間戳記 (例如「5 分鐘前」) 都由各自的控制器管理,該控制器會註冊私人瀏覽權限事件監聽器來更新時間。由於每個事件監聽器都將其控制器執行個體錨定至視窗根層級,因此在可見的即時通訊執行緒生命週期內,密集的有效閉包會占用大量記憶體。
// Ineffective pattern: Attaching global window listeners in individual UI controllers
export class RelativeTimeController {
constructor() {
this.visibilityListener = () => this.updateTime();
window.addEventListener("visibilitychange", this.visibilityListener);
}
// If dispose() fails to run or unregister, the closure pins the controller
}
修正方式:團隊集中管理全域視窗事件。共用的 TimerService 現在會在視窗上註冊一個可見度事件監聽器,並將更新內容廣播至所有有效的時間戳記控制器。這會立即將 126 個事件監聽器
縮減為 1 個
// Recommended: Centralized event coordination using a shared service
export class VisibilityService {
private listeners = new Set<() => void>();
constructor() {
// Single global listener throttled for the entire application
window.addEventListener("visibilitychange", () => this.notifyAll());
}
subscribe(callback: () => void): () => void {
this.listeners.add(callback);
return () => this.listeners.delete(callback); // Unsubscribe handle
}
}
第三方 SDK 和內嵌 iframe 預留位置
卸離的 DOM 節點是指已從文件樹狀結構中移除,但仍由 JavaScript 變數參照的元素。如果根變數保留了卸離 DOM 樹狀結構中的單一元素,則整個卸離樹狀結構都無法回收。
問題:當使用者在聊天室之間切換時,代理程式發現整個分離的 DOM 樹狀結構仍保留在記憶體中。並追蹤保留路徑,回到聊天室 UI 內嵌的媒體播放器。卸載聊天室時,UI 元件會從頁面中移除,但基礎媒體播放器 SDK 則不會明確刪除。第三方資料庫維護了有效 iframe ID 的內部全域字典,讓卸離的 DOM 保持運作。
修正方式:確保所有嵌入的 iframe 和第三方小工具都與元件處置生命週期連結。在播放器上明確呼叫 .destroy() 方法,可清除全域 SDK 登錄檔中的項目,並允許 DOM 進行垃圾收集。
// Recommended: Explicit teardown of embedded frames and SDK instances
export class MediaPlayerController {
private player: ThirdPartyPlayer | null = null;
dispose(): void {
if (this.player) {
this.player.destroy(); // Clears entry from global SDK registry
this.player = null;
}
}
}
其他模式
雖然上述三種模式在節省原始 MB 方面最具影響力,但代理程式也偵測並修正了超過 27 個其他洩漏。包括:
- 圖片陣列遙測:自訂診斷陣列追蹤不需捲動位置的圖片指標,導致分離的 DOM 樹狀結構保持運作
- 架構元件生命週期錯誤:自訂下拉式選單提升至文件主體,略過原始父項的卸載
- 隱含引數物件閉包陷阱:內部閉包不小心擷取外部函式的整個引數陣列
影響:主動記憶體管理,做為工作流程
Google Chat 團隊為程式設計代理配備代理專用 Chrome 開發人員工具,自動處理高度專業的工作。追蹤複雜的記憶體流失問題,從數天的人工堆積遍歷,縮短到幾分鐘的自動分析。
就像 P2ER 實施「強制經驗驗證」來強制執行 UI 穩定性一樣,團隊可以採用這種記憶體稽核方法,做為主動防護措施。開發人員通常會忽略記憶體問題,認為只有在非常大型或複雜的應用程式中才會發生。不過,如本個案研究所示,標準事件監聽器或靜態錯誤等常見模式可能會默默累積。將代理專用 Chrome 開發人員工具等工具整合至日常開發週期,AI 就能從基本的程式碼產生器,轉變為可靠的品質關卡,在無聲迴歸進入正式環境前就加以攔截。