Hoe Google Chat meer dan 30 geheugenproblemen heeft opgelost met Chrome DevTools voor agents

Vatan Aggarwal
Vatan Aggarwal Software Engineer

Gepubliceerd: 29 oktober 2026

Google Chat is een grote app met één pagina. Gebruikers laten de web-app regelmatig dagen of weken openstaan op hun browsertabblad. In langdurige apps is de status van het geheugen aan de clientzijde een belangrijk aandachtspunt. Niet-verzameld sluitingen en losgekoppelde DOM-boomstructuren bouwen zich langzaam op, wat uiteindelijk invloed heeft op de UI prestaties, pauzes voor garbage collection of tabbladcrashes op beperkte apparaten.

Het Google Chat-team heeft daarom een initiatief gelanceerd om geheugenproblemen op te sporen en op te lossen. Ze gebruikten Chrome DevTools voor agents, waaronder de DevTools MCP-server, om een moeilijk handmatig proces om te zetten in een geautomatiseerde workflow.

De uitdaging: een speld in een hooiberg van 500 MB

Als je precies wilt weten waarom een gelekt object in het geheugen wordt bewaard, moet je de bewaarpaden traceren via sterk verkleinde code. Hoewel recente verbeteringen aan het geheugenvenster van Chrome DevTools (bijvoorbeeld geavanceerde randfiltering en objectquery's) dit makkelijker maken, kan het handmatig uitvoeren van een momentopname van 500 MB nog steeds traag en mentaal vermoeiend zijn.

En om een lek te dichten, heb je 2 verschillende soorten kennis nodig:

  • Je moet begrijpen hoe de V8 JavaScript-engine omgaat met gecompileerde bytecode.
  • Je moet ook inzicht hebben in het specifieke statusbeheer en de levenscyclus van componenten van je app.

Het handmatig overbruggen van deze 2 verschillende sets kennis kost meestal dagen aan onderzoek en samenwerking met meerdere ontwikkelaars per lek.

De oplossing: de stapel doorzoeken met een agent

Het team realiseerde zich dat grote taalmodellen weliswaar heel goed zijn in redeneren over gestructureerde gegevens, maar dat ze de juiste tools nodig hebben om enorme bestanden te verwerken en tegelijkertijd het tokenverbruik redelijk te houden. Door Google Antigravity te koppelen aan de open-source Chrome DevTools MCP-server met geheugentools aan (--memoryDebugging=true), gaven ze de agent rechtstreekse programmatische toegang tot de V8-heap.

In plaats van dat een ontwikkelaar handmatig de retainer-bomen uitvouwt in het geheugenvenster van Chrome DevTools, voerde de agent de query rechtstreeks uit op de momentopname. Het team gebruikte een eenvoudige prompt om het proces te starten:

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

Met de vaardigheid Memory Debugging voerde de agent een gestructureerde workflow uit met specifieke MCP-tools:

  1. De momentopname vastleggen of laden: Gebruik tools zoals take_heapsnapshot of lees een bestaand bestand.
  2. De heap-structuur analyseren: De tool heeft gezocht naar veelvoorkomende bronnen van lekken met get_heapsnapshot_details en compare_heapsnapshots.
  3. Trace retaining paths (Behoudpaden traceren): Deze functie gebruikte get_heapsnapshot_retaining_paths om de dominator-structuur terug te lopen naar de garbage collection-roots.
  4. Oplossingen genereren: De tool vergeleek de bewaarpaden met de broncode van de app om zeer specifieke patches te genereren.

Technische inzichten: 3 veelvoorkomende patronen van geheugenlekken

De agent vond meer dan 30 verschillende lekken. De volgende 3 zijn de meest impactvolle patronen die de agent heeft gevonden. Ze vertegenwoordigen de grootste geheugenbesparingen en de meest voorkomende architecturale antipatronen in moderne web-apps.

De statische foutstacktrace-trap

De agent stelde vast dat het grootste lek werd veroorzaakt door vooraf toegewezen foutobjecten die permanent bijna 20 MB aan geheugen vasthielden. Een enkele statische foutconstante (EMPTY_KEY_ERROR) behield bijvoorbeeld 14, 1 MB op zichzelf.

Het probleem: In moderne JavaScript-engines zoals V8 wordt bij het maken van een Error-object een momentopname van de huidige aanroepstack gemaakt om te helpen bij het opsporen van fouten. De enorme behouden grootte is ontstaan door dit mechanisme:

  • De fout was een globale constante: De fout is geïnstantieerd tijdens de eerste module-evaluatie.
  • De JavaScript-engine heeft de stack vastgelegd: De actieve functie op de stack op dat moment was de belangrijkste initialisatiewrapper van de app (een Immediately Invoked Function Expression, of IIFE).
  • De array hield het geheugen vast: Omdat de app de fout nooit heeft opgeruimd, hield de interne stacktrace-array een permanente verwijzing naar de initialisatiefunctie van de hoofdbundel.
// 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;
  }
}

De oplossing: Het team heeft de code gewijzigd om fouten dynamisch te maken wanneer ze worden geactiveerd. Als een herbruikbare statische fout strikt noodzakelijk is, worden de vastgelegde interne V8-frames onmiddellijk vrijgegeven door de stack expliciet te wissen (EMPTY_KEY_ERROR.stack = '';).

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

Event-listeners voor globale vensters schalen

De agent heeft een probleem met geheugenschaling gevonden met visibilitychange gebeurtenislisteners. Hoewel dit geen strikt 'lek' is (omdat ze uiteindelijk worden opgeschoond wanneer de component wordt ontkoppeld), verhoogde dit het geheugengebruik omdat er één listener was voor elk bericht op het scherm. De momentopname onthulde 1142 ingebouwde gebeurtenislisteners, met 128 actieve listeners die rechtstreeks aan het algemene vensterobject waren gekoppeld.

Het probleem: In JavaScript zijn callbacks van event-listeners closures. Een closure behoudt verwijzingen naar de omsluitende lexicale scope om te zorgen dat de benodigde variabelen toegankelijk blijven. De agent herleidde 126 van deze listeners naar één component: de RelativeTimeController. Elke tijdstempel in de chat-UI (bijvoorbeeld '5 minuten geleden') beheerde een eigen controller, die een listener voor privézichtbaarheid registreerde om de tijd te updaten. Omdat elke listener de controllerinstantie aan de hoofdmap van het venster verankerde, nam een dicht web van actieve closures een aanzienlijke hoeveelheid geheugen in beslag gedurende de levensduur van het zichtbare chatgesprek.

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

De oplossing: Het team heeft wereldwijde venstergebeurtenissen gecentraliseerd. Een gedeelde TimerService registreert nu precies één zichtbaarheidslistener in het venster en zendt updates uit naar alle actieve tijdstempelcontrollers. Hierdoor werd het aantal luisteraars van 126 meteen teruggebracht tot precies 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 van derden en ingesloten iframe-retainers

Losgekoppelde DOM-nodes zijn elementen die uit de documentstructuur zijn verwijderd, maar waarnaar nog steeds wordt verwezen door een JavaScript-variabele. Als één element in een losgekoppelde DOM-boomstructuur wordt bewaard door een rootvariabele, kan de hele losgekoppelde boomstructuur niet worden teruggevorderd.

Het probleem: Wanneer gebruikers tussen chatruimtes navigeerden, ontdekte de agent dat volledige losgekoppelde DOM-boomstructuren in het geheugen bleven. Het pad leidde terug naar een ingesloten mediaspeler in de UI van de chatruimte. Als een chatruimte werd ontkoppeld, werd de UI-component van de pagina verwijderd, maar werd de onderliggende SDK van de mediaspeler nooit expliciet vernietigd. De bibliotheek van derden hield een interne algemene directory van actieve iframe-ID's bij, waardoor de losgekoppelde DOM actief bleef.

De oplossing: Zorg dat alle ingesloten iframes en widgets van derden zijn gekoppeld aan de levenscycli voor verwijdering van componenten. Als je de methode .destroy() expliciet aanroept op de speler, wordt de vermelding uit het algemene SDK-register gewist en kan de DOM worden opgeschoond.

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

Aanvullende patronen

De 3 eerder genoemde patronen hadden de grootste impact op het aantal bespaarde megabytes, maar de agent heeft ook meer dan 27 andere lekken opgespoord en verholpen. Dit zijn:

  • Telemetrie van afbeeldingsarrays: Waar losgekoppelde DOM-boomstructuren in leven werden gehouden door aangepaste diagnostische arrays die statistieken voor afbeeldingen boven de vouw bijhouden
  • Bugs in de levenscyclus van frameworkcomponenten: Waarbij aangepaste dropdownmenu's die naar de documenthoofdtekst werden verplaatst, de ontkoppeling van hun oorspronkelijke bovenliggende elementen omzeilden
  • Impliciete argumenten object closure trap: Waar inner closures onbedoeld de hele argumentenarray van outer functions vastlegden

Impact: Proactief geheugenbeheer als workflow

Door een programmeeragent uit te rusten met Chrome DevTools voor agents, heeft het Google Chat-team een zeer gespecialiseerde taak geautomatiseerd. Het opsporen van een complex geheugenlek ging van dagen handmatig heap-traversal naar minuten geautomatiseerde analyse.

Net zoals P2ER 'Verplichte empirische verificatie' heeft geïmplementeerd om de stabiliteit van de UI af te dwingen, kunnen teams deze aanpak voor geheugencontrole gebruiken als proactieve bescherming. Ontwikkelaars negeren geheugenproblemen vaak omdat ze ervan uitgaan dat deze alleen voorkomen in heel grote of complexe apps. Maar zoals deze casestudy laat zien, kunnen veelvoorkomende patronen zoals standaard gebeurtenislisteners of statische fouten zich ongemerkt opstapelen. Als je tools zoals Chrome DevTools voor agents integreert in je dagelijkse ontwikkelingscyclus, verandert AI van een eenvoudige codegenerator in een betrouwbare kwaliteitscontrole die stille regressies opspoort voordat ze de productie bereiken.