Google Chat 如何借助面向智能体的 Chrome 开发者工具修复 30 多个内存问题

Vatan Aggarwal
Vatan Aggarwal Software Engineer

发布时间: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 工具执行了结构化工作流:

  1. 捕获或加载快照:使用 take_heapsnapshot 等工具或读取现有文件。
  2. 分析堆结构:它使用 get_heapsnapshot_details 和 compare_heapsnapshots 扫描了常见的泄漏源。
  3. 跟踪保留路径:它使用 get_heapsnapshot_retaining_paths 遍历支配树,返回到垃圾回收根。
  4. 生成修复方案:它将保留路径与应用的源代码进行交叉对比,以输出高度具体的补丁。

技术洞察:三种常见的内存泄漏模式

该智能体识别出了 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 从基本的代码生成器转变为可靠的质量门,在静默回归问题进入生产环境之前将其捕获。