Dai heap snapshot “a tre scatti” ai percorsi di retainer: un flusso di lavoro pratico per individuare DOM staccati, listener dimenticati, crescita WASM e sprechi di stringhe.
Negli ultimi anni ci siamo abituati a sviluppare come se la memoria fosse una risorsa “comoda”: device potenti, RAM abbondante, framework che astraggono tutto. Poi arriva il conto: UI che diventano viscose dopo qualche navigazione, tab che consumano centinaia di MB, crash apparentemente inspiegabili. La buona notizia è che oggi il memory debugging può essere molto più sistematico (e meno doloroso), soprattutto se deleghi le parti ripetitive a un agente e tieni tu il controllo sulle scelte tecniche.
Di seguito trovi un flusso di lavoro concreto per scoprire leak, capire chi trattiene cosa e arrivare a una fix mirata.
DevTools for Agents: perché serve un’abilitazione esplicita
Gli strumenti di memory debugging pensati per lavorare con agenti sono una funzionalità avanzata e si attivano tramite configurazione (tipicamente aggiungendo un flag nel JSON dell’MCP server). L’idea è semplice: le analisi di memoria possono essere “costose” e non ha senso renderle disponibili (e attive) quando non servono.
Operativamente, quando l’abilitazione è corretta, l’agente può usare tool dedicati per:
- generare e confrontare heap snapshot;
- estrarre riepiloghi leggibili (invece di file enormi);
- analizzare percorsi di retainer (cioè chi sta impedendo il GC);
- individuare pattern specifici come stringhe duplicate.
Il metodo più affidabile: i tre heap snapshot
Il modo più robusto per confermare un leak lato JavaScript rimane il classico metodo a tre snapshot. Funziona bene perché evita falsi positivi dovuti a caching e oscillazioni normali dell’allocazione.
Un heap snapshot è una fotografia della memoria JS in un istante: oggetti, riferimenti, e soprattutto cosa li trattiene.
1) Snapshot “baseline”
Scatta un primo snapshot prima dell’azione sospetta.
Esempio: sospetti una perdita quando un componente viene smontato (unmount) o quando cambi route in una SPA.
2) Ripeti l’azione e torna indietro
Esegui l’azione e poi “annullala”:
- naviga alla view B;
- torna alla view A;
- ripeti se necessario per amplificare l’effetto.
3) Forza il Garbage Collection e scatta lo snapshot finale
Forza un GC (dove disponibile) e poi scatta il terzo snapshot: ciò che resta dopo il GC è molto più indicativo.
Perché servono tool di confronto “per agenti”
I file raw degli snapshot sono enormi: ingestibili per un agente e spesso poco pratici anche per una lettura manuale. La differenza la fanno strumenti che trasformano il confronto in un riassunto: l’attenzione va su ciò che cresce davvero in modo anomalo.
Il segnale più utile è la retained size delta positiva: memoria che avrebbe dovuto essere liberata e invece è ancora trattenuta.
Leak comuni che emergono subito
1) Detached DOM nodes: elementi rimossi ma ancora vivi
Un classico: rimuovi un elemento dal DOM (quindi non è più nella tree), ma esiste ancora un riferimento JavaScript che lo trattiene. Risultato: il nodo non viene garbage-collected.
Scenario tipico:
- modali, toast, overlay creati “al volo”;
- elementi appesi a variabili, store, cache, singleton;
- riferimenti chiusi in closure o registrati come listener.
Nota importante: staccare dal DOM non basta. Il GC non ragiona in termini di DOM visibile, ma di raggiungibilità via riferimenti.
E attenzione anche all’altro lato: a volte mantenere nodi “staccati” è una scelta intenzionale (riuso per performance). Non è automaticamente un bug: lo diventa quando cresce senza controllo e senza limiti.
2) Event listener non rimossi: la perdita più frequente nelle SPA
Pattern:
- il componente monta e fa
window.addEventListener(...); - in unmount non fa
removeEventListener; - la callback è una closure che trattiene lo scope del componente.
C’è un equivoco diffuso: “la closure trattiene solo le variabili usate”. In pratica spesso viene trattenuto più contesto del previsto, e basta un singolo oggetto “pesante” nello scope per impedire il rilascio di molta memoria.
Qui l’analisi vincente è il retaining path: partire dall’oggetto che ti aspetti morto (il componente o un suo sub-oggetto) e risalire alla catena che lo mantiene vivo. Quando il colpevole è un riferimento JS diretto, in DevTools spesso lo riconosci anche visivamente nel retainer tree.
Fix tipiche:
- rimuovere listener in teardown/unmount;
- evitare di chiudere su oggetti grandi (o spostarli fuori scope);
- usare
AbortControllere segnali di abort quando appropriato.
3) WebAssembly: memoria “invisibile” al GC
Con WebAssembly entri in un modello diverso: la memoria è una linear memory (concettualmente un grande ArrayBuffer). Il garbage collector JS non vede gli oggetti interni (C/C++/Rust) e quindi non può liberare ciò che non liberi esplicitamente.
Anche se non puoi “ispezionare” direttamente le strutture WASM come oggetti JS, c’è un indizio molto forte: l’agente può osservare che il buffer condiviso cresce costantemente. Questo è spesso sufficiente per capire dove indagare:
- allocazioni senza free;
- pool mai rilasciati;
- strategie di crescita troppo aggressive.
4) Stringhe duplicate: spreco silenzioso ma enorme
Non tutti i problemi di memoria sono leak: a volte è spreco strutturale. Un caso sorprendentemente comune è ripetere la stessa stringa in migliaia di oggetti.
Qui la soluzione è concettuale:
- string interning (riutilizzare un’unica istanza);
- normalizzare valori ripetuti (es. usare enum/ID);
- deduplicare a monte (parsing, mapping, caching).
Gli strumenti di analisi possono individuare le stringhe duplicate e quantificarne l’impatto, dandoti un bersaglio chiaro.
Un flusso di lavoro “da produzione”
Quando sospetti un leak reale, questo approccio riduce il rumore:
- Definisci un’azione ripetibile (route change, open/close dialog, mount/unmount).
- Esegui il metodo a tre snapshot (baseline → azione andata/ritorno → GC + snapshot).
- Confronta cercando grosse retained size delta positive.
- Per ogni candidato, usa i retaining paths: trova l’ancora che lo trattiene (listener, closure, cache globale, store, singleton).
- Applica fix mirate e ripeti: un leak corretto si vede perché la retained size delta torna a zero (o almeno smette di crescere a ogni ciclo).
Sintesi: la memoria non è infinita, ma il debugging può essere più “meccanico”
Il punto non è tornare a programmare come se avessimo pochi kilobyte: è evitare che una SPA moderna si comporti come se potesse consumare tutto senza conseguenze. Con il metodo a tre snapshot, il confronto guidato dalle retained size delta e l’analisi dei retaining paths, puoi trasformare un problema nebuloso (“dopo un po’ rallenta”) in una catena causale verificabile (“questo listener su window trattiene questa closure che trattiene questo componente”).
La ricaduta pratica è immediata: meno tab che esplodono in RAM, UI più reattive e bug più facili da riprodurre e correggere. In produzione, questo spesso vale più di qualsiasi micro-ottimizzazione.
Articolo originale: https://frontendfacile.it/blog/debug-della-memoria-con-agenti-come-scovare-leak-in-chrome-devtools-senza-impazz
Top comments (0)