Publié le 29 octobre 2026
Google Chat est une application monopage volumineuse. Les utilisateurs laissent régulièrement l'application Web ouverte dans l'onglet de leur navigateur pendant des jours, voire des semaines. Dans les applications de longue durée, l'état de la mémoire côté client est une préoccupation majeure. Les fermetures non collectées et les arbres DOM détachés s'accumulent lentement, ce qui finit par avoir un impact sur les performances de l'interface utilisateur, les pauses de récupération de mémoire ou les plantages d'onglets sur les appareils limités.
Pour résoudre ce problème, l'équipe Google Chat a lancé une initiative visant à détecter et à corriger les problèmes de mémoire. Ils ont utilisé les Outils de développement Chrome pour les agents, qui incluent le serveur MCP DevTools, pour transformer un processus manuel difficile en un workflow automatisé.
Le défi : une aiguille dans une botte de foin de 500 Mo
Pour identifier précisément pourquoi un objet ayant fuité est conservé en mémoire, il faut tracer les chemins de conservation à travers un code fortement minimisé. Bien que les récentes améliorations apportées au panneau "Mémoire" des outils pour les développeurs Chrome (par exemple, le filtrage avancé des bords et l'interrogation des objets) facilitent cette tâche, l'effectuer manuellement sur un instantané de 500 Mo peut toujours être lent et épuisant mentalement.
Pour corriger une fuite, deux types de connaissances distincts sont nécessaires :
- Vous devez comprendre comment le moteur JavaScript V8 gère le bytecode compilé.
- Vous devez également comprendre la gestion de l'état et le cycle de vie des composants spécifiques à votre application.
Le rapprochement manuel de ces deux ensembles de connaissances distincts nécessite généralement plusieurs jours d'investigation et de collaboration avec plusieurs développeurs par fuite.
La solution : parcourir le tas de mémoire avec un agent
L'équipe a réalisé que, même si les grands modèles de langage sont très efficaces pour raisonner sur des données structurées, ils ont besoin des bons outils pour traiter des fichiers volumineux tout en maintenant une consommation de jetons raisonnable. En connectant Google Antigravity au serveur MCP Open Source des Outils pour les développeurs Chrome avec les outils de mémoire activés (--memoryDebugging=true), ils ont donné à l'agent un accès programmatique direct au tas V8.
Au lieu qu'un développeur développe manuellement les arbres de rétention dans le panneau "Mémoire" des outils pour les développeurs Chrome, l'agent a interrogé directement l'instantané. L'équipe a utilisé un prompt simple pour lancer le processus :
Discover memory leaks in the heap snapshot chat.heapsnapshot using Chrome DevTools memory tools.
À l'aide de la compétence de débogage de la mémoire, l'agent a exécuté un workflow structuré à l'aide d'outils MCP spécifiques :
- Capturer ou charger l'instantané : à l'aide d'outils tels que
take_heapsnapshotou en lisant un fichier existant. - Analyser la structure du tas de mémoire : il a recherché les sources de fuites courantes à l'aide de
get_heapsnapshot_detailsetcompare_heapsnapshots. - Tracer les chemins de conservation : il a utilisé
get_heapsnapshot_retaining_pathspour remonter l'arbre des dominateurs jusqu'aux racines de récupération de mémoire. - Générer des correctifs : il a croisé les chemins de conservation avec le code source de l'application pour générer des correctifs très spécifiques.
Insights techniques : trois modèles courants de fuite de mémoire
L'agent a identifié plus de 30 fuites distinctes. Les trois modèles suivants sont les plus importants trouvés par l'agent. Ils représentent les économies de mémoire les plus importantes et les anti-modèles architecturaux les plus courants dans les applications Web modernes.
Le piège de la trace de la pile d'erreurs statique
L'agent a identifié que la plus grande fuite était liée à des objets d'erreur pré-alloués qui conservaient de manière permanente près de 20 Mo de mémoire. Par exemple, une seule constante d'erreur statique (EMPTY_KEY_ERROR) a conservé 14,1 Mo à elle seule.
Le problème : dans les moteurs JavaScript modernes comme V8, la création d'un objet Error capture un instantané de la pile d'appel actuelle pour faciliter le débogage. La taille conservée massive est due à ce mécanisme exact :
- L'erreur était une constante globale : elle a été instanciée lors de l'évaluation initiale du module.
- Le moteur JavaScript a capturé la pile : la fonction active sur la pile à ce moment-là était le wrapper d'initialisation principal de l'application (une expression de fonction immédiatement appelée, ou IIFE).
- Le tableau a piégé la mémoire : comme l'application n'a jamais récupéré l'erreur, son tableau de trace de la pile interne contenait une référence permanente à la fonction d'initialisation du bundle 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 solution : l'équipe a modifié le code pour créer des erreurs de manière dynamique lorsqu'elles sont générées.
Si une erreur statique réutilisable est strictement nécessaire, l'effacement explicite de la pile (EMPTY_KEY_ERROR.stack = '';) libère immédiatement les frames internes V8 capturés.
// 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
Scaling des écouteurs d'événements de fenêtre globale
L'agent a détecté un problème de scaling de la mémoire impliquant des écouteurs d'événements visibilitychange. Bien qu'il ne s'agisse pas d'une "fuite" stricte (car ils sont finalement nettoyés lorsque le composant est démonté), cela a augmenté l'utilisation de la mémoire, car il y avait un écouteur pour chaque message à l'écran. L'instantané a révélé 1 142 écouteurs d'événements intégrés, dont 128 écouteurs actifs associés directement à l'objet de fenêtre global.
Le problème : en JavaScript, les rappels d'écouteurs d'événements sont des fermetures. Une fermeture conserve les références à son champ d'application lexical englobant pour s'assurer que les variables dont elle a besoin restent accessibles. L'agent a retracé 126 de ces écouteurs jusqu'à un seul composant : RelativeTimeController. Chaque code temporel de l'interface utilisateur de Chat (par exemple, "Il y a 5 minutes") gérait son propre contrôleur, qui enregistrait un écouteur de visibilité privé pour mettre à jour l'heure. Étant donné que chaque écouteur ancrait son instance de contrôleur à la racine de la fenêtre, un réseau dense de fermetures actives mobilisait une quantité importante de mémoire pendant toute la durée de vie du fil de discussion 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 solution : l'équipe a centralisé les événements de fenêtre globaux. Un TimerService partagé enregistre désormais exactement un écouteur de visibilité sur la fenêtre et diffuse les mises à jour à tous les contrôleurs de code temporel actifs. Cela a instantanément réduit le nombre d'écouteurs de 126 à un seul.
// 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 tiers et intégration d'iFrame
Les nœuds DOM dissociés sont des éléments qui ont été supprimés de l'arborescence du document, mais qui sont toujours référencés par une variable JavaScript. Si un seul élément d'une arborescence DOM dissociée est conservé par une variable racine, l'intégralité de l'arborescence dissociée ne peut pas être récupérée.
Le problème : lorsque les utilisateurs naviguaient entre les espaces de chat, l'agent a constaté que des arbres DOM détachés entiers restaient en mémoire. Il a retracé le chemin de conservation jusqu'à un lecteur multimédia intégré dans l'UI de l'espace de discussion. Lorsqu'un espace de discussion était démonté, le composant d'interface utilisateur était supprimé de la page, mais le SDK du lecteur multimédia sous-jacent n'était jamais explicitement détruit. La bibliothèque tierce conservait un dictionnaire global interne des ID d'iFrame actifs, ce qui maintenait le DOM dissocié actif.
La solution : assurez-vous que tous les iFrames intégrés et les widgets tiers sont liés aux cycles de vie de suppression des composants. L'appel explicite de la méthode .destroy() sur le lecteur efface l'entrée du registre SDK global et permet au DOM d'être récupéré par le garbage collector.
// 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;
}
}
}
Autres formats
Bien que les trois modèles mentionnés précédemment aient été les plus efficaces en termes de mégaoctets bruts économisés, l'agent a également détecté et corrigé plus de 27 autres fuites. Par exemple :
- Télémétrie de tableau d'images : où les arbres DOM détachés ont été maintenus actifs par des tableaux de diagnostic personnalisés qui suivent les métriques d'image au-dessus de la ligne de flottaison
- Bugs liés au cycle de vie des composants du framework : les menus déroulants personnalisés hissés dans le corps du document contournaient le démontage de leurs parents d'origine.
- Piège de fermeture d'objet d'arguments implicites : lorsque des fermetures internes capturent involontairement l'intégralité du tableau d'arguments des fonctions externes
Impact : gestion proactive de la mémoire en tant que workflow
En équipant un agent de codage des Outils de développement Chrome pour les agents, l'équipe Google Chat a automatisé une tâche hautement spécialisée. Le traçage d'une fuite de mémoire complexe est passé de plusieurs jours de parcours manuel du tas de mémoire à quelques minutes d'analyse automatisée.
Tout comme P2ER a mis en œuvre la "vérification empirique obligatoire" pour assurer la stabilité de l'interface utilisateur, les équipes peuvent adopter cette approche d'audit de la mémoire comme garde-fou proactif. Les développeurs ignorent souvent les problèmes de mémoire, en supposant qu'ils ne se produisent que dans les applications très volumineuses ou complexes. Toutefois, comme le montre cette étude de cas, des modèles courants tels que les écouteurs d'événements standards ou les erreurs statiques peuvent s'accumuler de manière silencieuse. L'intégration d'outils tels que les outils de développement Chrome pour les agents dans votre cycle de développement quotidien transforme l'IA d'un simple générateur de code en un seuil de qualité fiable qui détecte les régressions silencieuses avant qu'elles n'atteignent la production.