Cómo Google Chat solucionó más de 30 problemas de memoria con las Herramientas para desarrolladores de Chrome para agentes

Vatan Aggarwal
Vatan Aggarwal Software Engineer

Publicado: 29 de octubre de 2026

Google Chat es una aplicación grande de una sola página. Los usuarios suelen dejar la app web abierta en la pestaña del navegador durante días o semanas. En las aplicaciones de larga duración, el estado de la memoria del cliente es una preocupación principal. Los cierres no recopilados y los árboles del DOM desconectados se acumulan lentamente, lo que, con el tiempo, afecta el rendimiento de la IU, las pausas de recolección de elementos no utilizados o las fallas de las pestañas en los dispositivos con limitaciones.

Para abordar este problema, el equipo de Google Chat lanzó una iniciativa para descubrir y corregir los problemas de memoria. Usaron las Herramientas para desarrolladores de Chrome para agentes, que incluyen el servidor de MCP de las Herramientas para desarrolladores, para convertir un proceso manual difícil en un flujo de trabajo automatizado.

El desafío: una aguja en un pajar de 500 MB

Para identificar exactamente por qué se conserva un objeto filtrado en la memoria, se deben rastrear las rutas de retención a través de código muy reducido. Si bien las mejoras recientes en el panel de memoria de las Herramientas para desarrolladores de Chrome (por ejemplo, el filtrado avanzado de bordes y la consulta de objetos) facilitan esta tarea, hacerlo de forma manual en una instantánea de 500 MB puede ser lento y agotador.

Para corregir una fuga, se requieren dos conjuntos de conocimientos distintos:

  • Debes comprender cómo el motor de JavaScript V8 controla el código de bytes compilado.
  • También debes comprender la administración de estado específica de tu aplicación y el ciclo de vida de los componentes.

Unir estos dos conjuntos de conocimientos distintos de forma manual suele llevar días de investigación y colaboración con varios desarrolladores por fuga.

La solución: Navega por el montón con un agente

El equipo se dio cuenta de que, si bien los modelos de lenguaje grandes son muy buenos para razonar sobre datos estructurados, necesitan las herramientas adecuadas para procesar archivos masivos y, al mismo tiempo, mantener un consumo de tokens razonable. Conectando Google Antigravity al servidor de MCP de código abierto de las Herramientas para desarrolladores de Chrome con las herramientas de memoria habilitadas (--memoryDebugging=true), le dieron al agente acceso programático directo al montón de V8.

En lugar de que un desarrollador expanda manualmente los árboles de retenedores en el panel de memoria de las Herramientas para desarrolladores de Chrome, el agente consultó la instantánea directamente. El equipo usó una instrucción sencilla para iniciar el proceso:

Discover memory leaks in the heap snapshot chat.heapsnapshot using Chrome DevTools memory tools.

Con la habilidad de depuración de memoria, el agente ejecutó un flujo de trabajo estructurado con herramientas de MCP específicas:

  1. Captura o carga la instantánea: Usa herramientas como take_heapsnapshot o lee un archivo existente.
  2. Analiza la estructura del montón: Se analizaron las fuentes de fugas comunes con get_heapsnapshot_details y compare_heapsnapshots.
  3. Rutas de retención de seguimiento: Se usó get_heapsnapshot_retaining_paths para recorrer el árbol de dominadores hasta las raíces de la recolección de elementos no utilizados.
  4. Generar correcciones: Se compararon las rutas de retención con el código fuente de la aplicación para generar parches muy específicos.

Información técnica: Tres patrones comunes de fuga de memoria

El agente identificó más de 30 filtraciones distintas. Los siguientes tres son los patrones más impactantes que encontró el agente, que representan los mayores ahorros de memoria y los antipatrones arquitectónicos más comunes en las aplicaciones web modernas.

La trampa del seguimiento de pila de errores estáticos

El agente identificó que la fuga individual más grande estaba vinculada a objetos de error preasignados que retenían de forma permanente casi 20 MB de memoria. Por ejemplo, una sola constante de error estática (EMPTY_KEY_ERROR) retuvo 14.1 MB por sí sola.

El problema: En los motores de JavaScript modernos, como V8, la creación de un objeto Error captura una instantánea de la pila de llamadas actual para ayudar en la depuración. El tamaño retenido masivo se produjo debido a este mecanismo exacto:

  • El error era una constante global: Se creó una instancia durante la evaluación inicial del módulo.
  • El motor de JavaScript capturó la pila: La función activa en la pila en ese momento era el wrapper de inicialización principal de la aplicación (una expresión de función invocada de inmediato o IIFE).
  • El array atrapó la memoria: Debido a que la aplicación nunca recolectó el error, su array de seguimiento de pila interno mantuvo una referencia permanente a la función de inicialización del paquete principal.
// 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;
  }
}

La solución: El equipo cambió el código para crear errores de forma dinámica cuando se producen. Si es estrictamente necesario un error estático reutilizable, borrar de forma explícita la pila (EMPTY_KEY_ERROR.stack = '';) libera de inmediato los marcos internos de V8 capturados.

// 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

Cómo escalar los objetos de escucha de eventos de ventana global

El agente encontró un problema de escalamiento de memoria que involucra objetos de escucha de eventos visibilitychange. Si bien no se trata de una "fuga" estricta (ya que, con el tiempo, se limpian cuando se desmonta el componente), aumentó el uso de memoria porque había un objeto de escucha para cada mensaje en la pantalla. La instantánea reveló 1,142 objetos de escucha de eventos integrados, con 128 objetos de escucha activos conectados directamente al objeto de ventana global.

El problema: En JavaScript, las devoluciones de llamada de los objetos de escucha de eventos son cierres. Un cierre conserva las referencias a su alcance léxico adjunto para garantizar que las variables que necesita sigan siendo accesibles. El agente rastreó 126 de estos objetos de escucha hasta un solo componente: RelativeTimeController. Cada marca de tiempo en la IU de Chat (por ejemplo, "Hace 5 minutos") administraba su propio controlador, que registraba un objeto de escucha de visibilidad privado para actualizar la hora. Como cada objeto de escucha anclaba su instancia de controlador a la raíz de la ventana, una red densa de cierres activos ocupaba una cantidad significativa de memoria durante el ciclo de vida del hilo de chat visible.

// 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
}

La solución: El equipo centralizó los eventos de ventana globales. Un TimerService compartido ahora registra exactamente un objeto de escucha de visibilidad en la ventana y transmite actualizaciones a todos los controladores de marcas de tiempo activos. Esto redujo instantáneamente 126 objetos de escucha a exactamente 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
  }
}

Retenedores de SDK de terceros y de iframe incorporado

Los nodos del DOM desconectados son elementos que se quitaron del árbol del documento, pero a los que aún hace referencia una variable de JavaScript. Si una variable raíz retiene un solo elemento de un árbol del DOM desconectado, no se puede recuperar todo el árbol desconectado.

El problema: Cuando los usuarios navegaban entre los espacios de chat, el agente descubrió que los árboles del DOM desconectados completos permanecían en la memoria. Se rastreó la ruta de retención hasta un reproductor multimedia incorporado en la IU del espacio de chat. Cuando se desmontaba un espacio de chat, el componente de la IU se quitaba de la página, pero el SDK del reproductor de contenido multimedia subyacente nunca se destruía de forma explícita. La biblioteca de terceros mantenía un diccionario global interno de IDs de iframe activos, lo que mantenía vivo el DOM separado.

La solución: Asegúrate de que todos los iframes incorporados y los widgets de terceros se vinculen a los ciclos de vida de eliminación de componentes. Llamar de forma explícita al método .destroy() en el reproductor borra la entrada del registro global del SDK y permite que el DOM se recolecte como elemento no utilizado.

// 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;
    }
  }
}

Patrones adicionales

Si bien los tres patrones mencionados antes fueron los más impactantes en términos de megabytes sin procesar ahorrados, el agente también detectó y corrigió más de 27 fugas adicionales. Entre ellas, se incluyen las siguientes:

  • Telemetría de arrays de imágenes: Es donde los arrays de diagnóstico personalizados que hacen un seguimiento de las métricas de imágenes de la mitad superior de la página mantuvieron activos los árboles del DOM desconectados.
  • Errores del ciclo de vida de los componentes del framework: En los casos en que los menús desplegables personalizados elevados al cuerpo del documento omitían el desmontaje de sus elementos superiores originales
  • Trampa de cierre de objeto de argumentos implícitos: En los casos en que los cierres internos capturaron de forma involuntaria todo el array de argumentos de las funciones externas

Impacto: Administración proactiva de la memoria como flujo de trabajo

Equipando un agente de programación con las Herramientas para desarrolladores de Chrome para agentes, el equipo de Google Chat automatizó una tarea altamente especializada. El seguimiento de una fuga de memoria compleja se redujo de días de recorrido manual del montón a minutos de análisis automatizado.

Al igual que P2ER implementó la "verificación empírica obligatoria" para aplicar la estabilidad de la IU, los equipos pueden adoptar este enfoque de auditoría de memoria como una protección proactiva. Los desarrolladores suelen descartar los problemas de memoria, ya que suponen que solo ocurren en aplicaciones muy grandes o complejas. Sin embargo, como se muestra en este caso de éxito, los patrones comunes, como los objetos de escucha de eventos estándar o los errores estáticos, pueden acumularse de forma silenciosa. Integrar herramientas como las Herramientas para desarrolladores de Chrome para agentes en tu ciclo de desarrollo diario transforma la IA de un generador de código básico en un punto de control de calidad confiable que detecta regresiones silenciosas antes de que lleguen a producción.