发布时间:2026 年 10 月 29 日
Google Chat 是一个大型单页应用。用户经常在浏览器标签页中打开 Web 应用,并让其保持打开状态数天或数周。在长期运行的应用中,客户端内存运行状况是首要关注的问题。未收集的闭包和分离的 DOM 树会缓慢累积,最终影响界面性能、垃圾回收暂停或受限设备上的标签页崩溃。
为了解决这个问题,Google Chat 团队发起了一项计划,旨在发现并修复内存问题。他们使用面向智能体的 Chrome 开发者工具(其中包含开发者工具 MCP 服务器),将繁琐的手动流程转变为自动化工作流。
挑战:大海捞针,在 500 MB 的数据中寻找关键信息
若要确切找出泄漏的对象为何保留在内存中,需要通过高度缩减的代码跟踪保留路径。虽然 Chrome 开发者工具内存面板最近的改进(例如高级边缘过滤和对象查询)使这项工作变得更加轻松,但手动对 500 MB 的快照执行此操作仍然可能很慢,并且会让人感到心力交瘁。
修复泄漏需要两套不同的知识:
- 您需要了解 V8 JavaScript 引擎如何处理已编译的字节码。
- 您还需要了解应用的特定状态管理和组件生命周期。
手动将这两组不同的知识联系起来,通常需要花费数天时间进行调查,并与多位开发者协作,才能解决每个泄漏问题。
解决方案:使用代理浏览堆
团队意识到,虽然大语言模型非常擅长对结构化数据进行推理,但它们需要合适的工具来处理海量文件,同时将 token 消耗控制在合理范围内。通过将 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 多处不同的泄漏。以下三种是智能体发现的最具影响力的模式,它们代表了现代 Web 应用中最大的内存节省和最常见的架构反模式。
静态错误堆栈轨迹陷阱
代理发现,最大的单次泄漏与预分配的错误对象有关,这些对象永久保留了近 20 MB 的内存。例如,单个静态错误常量 (EMPTY_KEY_ERROR) 本身就保留了 14.1 MB。
问题:在 V8 等现代 JavaScript 引擎中,创建 Error 对象会捕获当前调用堆栈的快照,以帮助进行调试。正是由于这种机制,才导致了保留大小过大:
- 错误是全局常量:它是在初始模块评估期间实例化的。
- JavaScript 引擎捕获了堆栈:当时堆栈上的活跃函数是应用的主初始化封装容器(立即调用的函数表达式,即 IIFE)。
- 数组捕获了内存:由于应用从未对错误进行垃圾回收,因此其内部堆栈轨迹数组保留了对主 bundle 初始化函数的永久引用。
// 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 个活跃监听器直接附加到全局 window 对象。
问题:在 JavaScript 中,事件监听器回调是闭包。闭包会保留对其封闭词法范围的引用,以确保其所需的变量仍可访问。代理将其中 126 个监听器追踪到了一个组件:RelativeTimeController。Chat 界面中的每个时间戳(例如“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 树仍保留在内存中。它将保留路径追溯到 Chat 聊天室界面中的嵌入式媒体播放器。当聊天空间卸载时,界面组件会从页面中移除,但底层的媒体播放器 SDK 永远不会被显式销毁。第三方库维护了一个包含活跃 iframe ID 的内部全局字典,从而使已分离的 DOM 保持活跃状态。
解决方法:确保所有嵌入式 iframe 和第三方 widget 都与组件处置生命周期相关联。在播放器上显式调用 .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;
}
}
}
其他模式
虽然前面提到的三种模式在节省原始兆字节方面影响最大,但该代理还检测并修复了超过 27 处其他泄漏。 其中包括:
- 图片数组遥测:通过自定义诊断数组跟踪首屏图片指标,使分离的 DOM 树保持活跃
- 框架组件生命周期 bug:提升到文档正文的自定义下拉菜单绕过了其原始父级的卸载
- 隐式参数对象闭包陷阱:内部闭包无意中捕获了外部函数的整个参数数组
影响:主动内存管理作为工作流
通过为编码智能体配备 Chrome DevTools for Agents,Google Chat 团队成功将一项高度专业化的任务自动化。追踪复杂的内存泄漏问题,从数天的人工堆遍历缩短到数分钟的自动化分析。
就像 P2ER 实施“强制性经验验证”来强制执行界面稳定性一样,团队也可以采用这种内存审核方法作为主动防护措施。开发者通常会忽视内存问题,认为这些问题只发生在非常庞大或复杂的应用中。不过,正如本案例研究所示,标准事件监听器或静态错误等常见模式可能会悄无声息地累积。 将 Chrome 开发者工具等智能体工具集成到日常开发周期中,可将 AI 从基本的代码生成器转变为可靠的质量门,在静默回归问题进入生产环境之前将其捕获。