Come Google Chat ha risolto oltre 30 problemi di memoria con Chrome DevTools per agenti

Vatan Aggarwal
Vatan Aggarwal Software Engineer

Data di pubblicazione: 29 ottobre 2026

Google Chat è un'applicazione a pagina singola di grandi dimensioni. Gli utenti lasciano regolarmente l'app web aperta nella scheda del browser per giorni o settimane. Nelle applicazioni di lunga durata, l'integrità della memoria lato client è una preoccupazione principale. Le chiusure non raccolte e gli alberi DOM scollegati si accumulano lentamente, con un impatto sulle prestazioni della UI, sulle pause della garbage collection o sugli arresti anomali delle schede sui dispositivi con limitazioni.

Per risolvere questo problema, il team di Google Chat ha lanciato un'iniziativa per scoprire e risolvere i problemi di memoria. Hanno utilizzato Chrome DevTools per agenti, che include il server MCP di DevTools, per trasformare un processo manuale difficile in un workflow automatizzato.

La sfida: un ago in un pagliaio da 500 MB

Per identificare esattamente il motivo per cui un oggetto con leak viene mantenuto in memoria, è necessario tracciare i percorsi di conservazione attraverso un codice fortemente minimizzato. Sebbene i recenti miglioramenti al riquadro Memoria di Chrome DevTools (ad esempio, il filtro avanzato dei bordi e l'esecuzione di query sugli oggetti) semplifichino questa operazione, eseguirla manualmente su uno snapshot da 500 MB può comunque essere un'operazione lenta e mentalmente faticosa.

Per risolvere una fuga di dati sono necessarie due serie di conoscenze distinte:

  • Devi capire in che modo il motore JavaScript V8 gestisce il bytecode compilato.
  • Devi anche comprendere la gestione dello stato e il ciclo di vita dei componenti specifici della tua applicazione.

Il collegamento manuale di questi due distinti insiemi di conoscenze richiede in genere giorni di indagini e collaborazione con più sviluppatori per ogni fuga di dati.

La soluzione: esplora l'heap con un agente

Il team si è reso conto che, sebbene i modelli linguistici di grandi dimensioni siano molto efficaci nel ragionare su dati strutturati, hanno bisogno degli strumenti giusti per elaborare file di grandi dimensioni mantenendo un consumo di token ragionevole. Collegando Google Antigravity al server MCP open source di Chrome DevTools con gli strumenti di memoria attivati (--memoryDebugging=true), hanno dato all'agente accesso programmatico diretto allo heap V8.

Anziché chiedere a uno sviluppatore di espandere manualmente gli alberi di contenimento nel riquadro Memoria di Chrome DevTools, l'agente ha eseguito una query direttamente sullo snapshot. Il team ha utilizzato un prompt semplice per avviare il processo:

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

Utilizzando la competenza di debug della memoria, l'agente ha eseguito un workflow strutturato utilizzando strumenti MCP specifici:

  1. Acquisisci o carica l'istantanea: utilizza strumenti come take_heapsnapshot o leggi un file esistente.
  2. Analizza la struttura dell'heap: ha cercato fonti comuni di leak utilizzando get_heapsnapshot_details e compare_heapsnapshots.
  3. Traccia i percorsi di conservazione: ha utilizzato get_heapsnapshot_retaining_paths per risalire l'albero dei dominatori fino alle radici della garbage collection.
  4. Generazione di correzioni: ha eseguito un controllo incrociato dei percorsi di conservazione con il codice sorgente dell'applicazione per generare patch altamente specifiche.

Insight tecnici: tre pattern comuni di perdita di memoria

L'agente ha identificato oltre 30 fughe distinte. I tre seguenti sono i pattern più significativi trovati dall'agente, che rappresentano i maggiori risparmi di memoria e gli anti-pattern architetturali più comuni nelle moderne applicazioni web.

La trappola dell'analisi dello stack di errori statici

L'agente ha identificato che la singola perdita più grande era legata a oggetti di errore preallocati che conservavano in modo permanente quasi 20 MB di memoria. Ad esempio, una singola costante di errore statica (EMPTY_KEY_ERROR) ha conservato 14,1 MB da sola.

Il problema: nei moderni motori JavaScript come V8, la creazione di un oggetto Error acquisisce uno snapshot dello stack di chiamate corrente per facilitare il debug. La dimensione mantenuta enorme si è verificata proprio a causa di questo meccanismo:

  • L'errore era una costante globale: è stata creata un'istanza durante la valutazione iniziale del modulo.
  • Il motore JavaScript ha acquisito lo stack: la funzione attiva nello stack in quel momento era il wrapper di inizializzazione principale dell'applicazione (un'espressione di funzione immediatamente richiamata o IIFE).
  • L'array ha intrappolato la memoria: poiché l'applicazione non ha mai eseguito la garbage collection dell'errore, il suo array di analisi dello stack interno ha mantenuto un riferimento permanente alla funzione di inizializzazione del bundle principale.
// 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 correzione: il team ha modificato il codice per creare errori in modo dinamico quando vengono generati. Se è strettamente necessario un errore statico riutilizzabile, la cancellazione esplicita dello stack (EMPTY_KEY_ERROR.stack = '';) libera immediatamente i frame interni V8 acquisiti.

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

Scalabilità dei listener di eventi della finestra globale

L'agente ha rilevato un problema di scalabilità della memoria che coinvolge visibilitychange listener di eventi. Sebbene non si tratti di un vero e proprio "leak" (poiché alla fine vengono liberati quando il componente viene smontato), la memoria utilizzata è aumentata perché era presente un listener per ogni messaggio sullo schermo. L'istantanea ha rivelato 1142 listener di eventi integrati, con 128 listener attivi collegati direttamente all'oggetto finestra globale.

Il problema: in JavaScript, i callback dei listener di eventi sono chiusure. Una chiusura conserva i riferimenti al suo ambito lessicale di inclusione per garantire che le variabili di cui ha bisogno rimangano accessibili. L'agente ha ricondotto 126 di questi listener a un singolo componente: RelativeTimeController. Ogni timestamp nell'UI di Chat (ad esempio "5 minuti fa") gestiva il proprio controller, che registrava un listener di visibilità privato per aggiornare l'ora. Poiché ogni listener ancorava la propria istanza del controller alla radice della finestra, una fitta rete di chiusure attive occupava una quantità significativa di memoria per tutta la durata del thread di chat visibile.

// 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 soluzione: il team ha centralizzato gli eventi globali della finestra. Un TimerService condiviso ora registra esattamente un listener di visibilità sulla finestra e trasmette gli aggiornamenti a tutti i controller dei timestamp attivi. In questo modo, i 126 listener sono stati ridotti a uno solo.

// 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 di terze parti e iframe incorporati

I nodi DOM scollegati sono elementi che sono stati rimossi dall'albero del documento, ma a cui fa ancora riferimento una variabile JavaScript. Se un singolo elemento in un albero DOM scollegato viene conservato da una variabile radice, l'intero albero scollegato non può essere recuperato.

Il problema: quando gli utenti navigavano tra gli spazi di chat, l'agente ha rilevato che interi alberi DOM scollegati rimanevano in memoria. Ha tracciato il percorso di conservazione fino a un media player incorporato nell'UI dello spazio di Chat. Quando uno spazio di chat veniva smontato, il componente UI veniva rimosso dalla pagina, ma l'SDK del media player sottostante non veniva mai eliminato esplicitamente. La libreria di terze parti manteneva un dizionario globale interno degli ID iframe attivi, mantenendo attivo il DOM scollegato.

La soluzione: assicurati che tutti gli iframe incorporati e i widget di terze parti siano collegati ai cicli di vita di eliminazione dei componenti. La chiamata esplicita del metodo .destroy() sul player cancella la voce dal registro SDK globale e consente la garbage collection del 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;
    }
  }
}

Pattern aggiuntivi

Sebbene i tre pattern menzionati in precedenza siano stati i più significativi in termini di megabyte non elaborati risparmiati, l'agente ha anche rilevato e corretto oltre 27 altri leak. Tra questi:

  • Telemetria degli array di immagini: gli alberi DOM scollegati venivano mantenuti attivi da array di diagnostica personalizzati che monitoravano le metriche delle immagini above the fold.
  • Bug del ciclo di vita dei componenti del framework: i menu a discesa personalizzati inseriti nel corpo del documento ignoravano lo smontaggio dei relativi elementi principali originali.
  • Trap di chiusura dell'oggetto degli argomenti impliciti: dove le chiusure interne hanno acquisito involontariamente l'intero array di argomenti delle funzioni esterne

Impatto: gestione proattiva della memoria come workflow

Dotando un agente di programmazione di Chrome DevTools per agenti, il team di Google Chat ha automatizzato un'attività altamente specializzata. Il tracciamento di una perdita di memoria complessa è passato da giorni di attraversamento manuale dell'heap a minuti di analisi automatizzata.

Proprio come P2ER ha implementato la "verifica empirica obbligatoria" per garantire la stabilità della UI, i team possono adottare questo approccio di controllo della memoria come misura di protezione proattiva. Gli sviluppatori spesso ignorano i problemi di memoria, pensando che si verifichino solo in applicazioni molto grandi o complesse. Tuttavia, come mostra questo case study, pattern comuni come listener di eventi standard o errori statici possono accumularsi silenziosamente. L'integrazione di strumenti come Chrome DevTools per agenti nel ciclo di sviluppo quotidiano trasforma l'AI da un generatore di codice di base in un quality gate affidabile che rileva le regressioni silenziose prima che raggiungano la produzione.