Smetti di Far Perdere Memoria con WeakMap
Riparare codice debole con riferimenti deboli!
Hai presente quella sensazione quando cambi una riga di codice e vedi l’utilizzo di memoria calare del 50%? Ho avuto quel momento guardando il Performance Monitor di Chrome DevTools mentre un’app dashboard passava da un’emorragia di 100MB all’ora a funzionare pulita per un intero pomeriggio.
La modifica di una riga: new Map() è diventato new WeakMap().
Tutto qui. Stessa API, stesso schema d’uso, comportamento completamente diverso sotto il cofano. Ma capire perché funziona significa comprendere qualcosa a cui la maggior parte degli sviluppatori JavaScript non pensa mai: cosa succede quando nessuno sta più guardando i tuoi dati.
Quando i Riferimenti Diventano Ancore
Una Map normale in JavaScript tratta le sue chiavi come un carico prezioso. Una volta che ci metti qualcosa, la Map ci si aggrappa con una presa d’acciaio. Il Garbage Collector vede questa relazione e pensa: “Chiaramente hanno ancora bisogno di questo oggetto, meglio non toccarlo.”
Questo istinto protettivo diventa un problema quando memorizzi metadati su cose temporanee. Nodi DOM che vengono rimossi. Sessioni utente che scadono. Istanze di componenti che smontano. La Map non sa che questi oggetti non sono più utili. Sa solo di avere un riferimento, quindi li tiene in vita.
const cache = new Map();
function trackClick(element) { cache.set(element, { clicks: 0 });}
document.body.removeChild(element);// L'elemento non è più nel DOM, ma cache lo tiene in memoriaIl Garbage Collector non può ripulire element perché cache punta ancora ad esso. Questo si chiama “riferimento forte”, e nelle Single Page Application a lunga esecuzione diventa una perdita che alla fine fa crashare il browser.
WeakMap Cambia le Regole
Una WeakMap funziona diversamente. Tratta le sue chiavi come cittadini temporanei piuttosto che residenti permanenti. Quando memorizzi qualcosa in una WeakMap, stai essenzialmente dicendo: “Voglio associare questi dati a questo oggetto, ma non voglio essere la ragione per cui resta in vita.”
Se l’unica cosa che tiene in memoria un oggetto è una WeakMap, il Garbage Collector può prenderlo. Quando l’oggetto scompare, l’entry nella WeakMap scompare con esso. Nessuna pulizia manuale necessaria.
const cache = new WeakMap();
function trackClick(element) { cache.set(element, { clicks: 0 });}
document.body.removeChild(element);// L'elemento viene Garbage Collected// L'entry nella cache scompare automaticamenteHo eseguito un benchmark creando 100.000 nodi DOM, memorizzando metadati su ciascuno, poi rimuovendoli tutti. Con una Map, il browser tratteneva 150-200MB. Con una WeakMap, è sceso a 70-80MB. Stesso codice, stessa funzionalità, metà impronta di memoria.
Cosa Perdi per Strada
WeakMap ha vincoli che sembrano limitazioni finché non capisci che sono ciò che rende possibile la magia.
Non puoi iterare su una WeakMap. Niente forEach, niente keys(), niente values(). Ha senso se ci pensi: il Garbage Collector potrebbe cancellare un’entry nel bel mezzo del tuo ciclo. Vuoi davvero dover gestire questa situazione?
Non puoi controllare la dimensione. Niente proprietà .size, niente .length. Di nuovo, è un bersaglio mobile. Il numero potrebbe cambiare tra quando chiedi e quando ottieni la risposta.
Le chiavi devono essere oggetti. Niente stringhe, niente numeri, niente primitivi. Questo è fondamentale per come funzionano i riferimenti deboli: i valori primitivi non hanno un’identità che possa essere tracciata separatamente dal loro valore.
Questi non sono bug. Sono il design. WeakMap è costruito per un lavoro specifico: associare metadati a oggetti senza impedire che quegli oggetti vengano ripuliti. Se hai bisogno di iterazione, chiavi primitive o un conteggio delle entry, probabilmente stai risolvendo un problema diverso e dovresti usare una Map normale.
Dove Questo Aiuta Davvero
Il pattern “dati privati” era il caso d’uso originale di WeakMap, prima che JavaScript avesse i campi #private. Le librerie creavano una WeakMap fuori dalla classe e la usavano per memorizzare dati che non dovessero essere accessibili sull’istanza.
const privateData = new WeakMap();
class User { constructor(name) { privateData.set(this, { name }); }
getName() { return privateData.get(this).name; }}Quando un’istanza di User viene Garbage Collected, i dati privati vanno con essa. Nessun codice di pulizia necessario.
La memoizzazione è un’altra applicazione naturale, specialmente quando cachei risultati basati su input oggetto piuttosto che valori primitivi. Se il tuo calcolo costoso prende un oggetto config come input, una WeakMap significa che non devi preoccuparti che la cache sopravviva ai config.
const cache = new WeakMap();
function expensiveCalc(obj) { if (cache.has(obj)) return cache.get(obj);
const result = heavyMath(obj); cache.set(obj, result); return result;}La cache vive solo finché vivono gli oggetti memorizzati. Una volta che obj non è più referenziato da nessun’altra parte, sia il risultato cacheato che l’entry nella cache scompaiono insieme.
Quando Usarla
Le perdite di memoria nelle app web moderne di solito derivano da riferimenti obsoleti a cose che dovrebbero essere state ripulite. Se stai costruendo qualcosa a lunga esecuzione — una dashboard che resta aperta tutto il giorno, un’app chat che funziona per ore, un pannello di amministrazione che non si ricarica mai — devi pensare a cosa succede ai dati vecchi.
WeakMap è particolarmente utile quando associ dati a nodi DOM, istanze di componenti, o qualsiasi oggetto il cui ciclo di vita non controlli. Se stai memorizzando qualcosa basandoti su un riferimento e quel riferimento potrebbe sparire, WeakMap rende la pulizia molto più semplice.
La Map normale è ancora la scelta giusta quando stai costruendo una cache vera con politiche di espulsione, quando hai bisogno di iterare sulle entry, quando usi chiavi primitive, o quando sono i dati stessi ad essere importanti piuttosto che la loro associazione con un oggetto.
La cosa bella di WeakMap è che di solito è ovvio quando ti serve. Se ti ritrovi a scrivere codice di pulizia per rimuovere entry dalla map quando gli oggetti vengono distrutti, questo è un segnale. Se sei preoccupato per una crescita illimitata della memoria perché non sai quando eliminare le cose, questo è un altro segnale.
A volte la funzionalità migliore è quella che funziona senza che tu debba pensarci.
Risorse
- MDN: WeakMap
- MDN: Gestione della Memoria
- V8 Blog: Riferimenti Deboli e Finalizzatori
- JavaScript.info: WeakMap e WeakSet