modulo 09 / lezione 47

Performance, memoria e osservabilità

Memory leak e risorse non rilasciate

Riconoscere crescita anomala della memoria, listener attivi, timer pendenti, sottoscrizioni store e riferimenti trattenuti.

55 min Debugging

In parole semplici

Un memory leak è memoria che non viene rilasciata anche quando non serve più. In linguaggi con garbage collector (JS, Python, Java), la GC libera la memoria solo se nessuno trattiene un riferimento. Un timer dimenticato, un listener mai rimosso, una sottoscrizione attiva: tutti sono “fili” che impediscono al GC di pulire.

In single-page application, il sintomo classico: navigando tra le pagine, l’app diventa progressivamente più lenta. Memoria che cresce ad ogni vista visitata. Eventualmente, crash.

Parole nuove

  • garbage collector (GC): meccanismo che libera memoria automaticamente.
  • reference: riferimento a un oggetto. Finché esiste, GC non lo libera.
  • heap snapshot: fotografia dell’heap in un istante.
  • retained size: memoria che un oggetto trattiene direttamente o indirettamente.
  • leak vs growth: leak = mai liberato. Growth = libera lento.

Cosa impari

  • Identificare i 5 schemi di leak più frequenti.
  • Acquisire e confrontare heap snapshot.
  • Usare il Memory panel di DevTools.

I cinque schemi di leak in JavaScript

1. Listener non rimosso

window.addEventListener("scroll", handler);
// componente viene smontato, listener resta

Conseguenza: handler mantiene riferimenti ai componenti vecchi. GC non può liberarli.

Fix: cleanup. In React useEffect return:

useEffect(() => {
  window.addEventListener("scroll", handler);
  return () => window.removeEventListener("scroll", handler);
}, []);

2. Timer pendente

const id = setInterval(() => aggiorna(), 1000);
// componente smontato, timer continua

Fix:

return () => clearInterval(id);

Stesso per setTimeout se sopravvive al componente.

3. Closure che cattura troppo

function setupHeavy(data) {
  return () => console.log("solo log");
  // ma `data` è catturato anche se non usato esplicitamente
}

In alcuni engine, il garbage collector è “ottimista” e non riconosce subito che data non è usato. Soluzione: rilascia esplicitamente.

function setupHeavy(data) {
  const messaggio = data.summary;
  data = null;  // libera la closure
  return () => console.log(messaggio);
}

4. Sottoscrizione store / observable non chiusa

const sub = store.subscribe(stato => aggiorna(stato));
// componente smontato, subscription resta

Fix:

return () => sub.unsubscribe();

In RxJS, takeUntil(destroy$) è il pattern canonico.

5. Map / Set di lunga vita che accumulano

const cache = new Map();
function get(id) {
  if (!cache.has(id)) cache.set(id, fetchHeavy(id));
  return cache.get(id);
}

Niente eviction = leak. Soluzioni:

  • WeakMap se le chiavi sono oggetti.
  • LRU cache con dimensione massima.
  • TTL e cleanup periodico.

Heap snapshot: come si fa

Chrome DevTools → tab Memory → “Heap snapshot” → “Take snapshot”.

Workflow per indagare leak:

  1. Snapshot 1: stato iniziale.
  2. Esegui un’azione che dovrebbe liberare (es. apri e chiudi una vista 5 volte).
  3. Snapshot 2.
  4. Confronto: oggetti “Detached” o “Constructor” con count crescente.

Specifico: filtra per “Detached” e cerca DOM o componenti che dovrebbero essere stati eliminati ma sono ancora trattenuti da qualche referenza.

Allocation timeline

Tab Memory → “Allocation instrumentation on timeline”:

  • Registri una sessione.
  • Vedi un istogramma: ogni colonna è un blocco di allocazioni.
  • Le colonne azzurre rimangono dopo GC → potenziale leak.

Click su una colonna → vedi quali oggetti sono stati allocati e da quale callstack.

Profiler memoria di Node.js

node --inspect app.js

Chrome a chrome://inspect. Snapshot funzionano come browser.

In produzione, clinic.js o 0x permettono profili memoria batch.

Esempio realistico

Pagina con un component “Mappa” che apre WebGL. Navighi: mappa1, lista, mappa2, lista, mappa3, lista. La memoria cresce.

Indagine:

  1. Snapshot tra mappa e lista. Confronto.
  2. Vedi “WebGLContext” count > 1 dopo aver chiuso la mappa.
  3. Indagine: chi mantiene riferimento al canvas?
  4. Trovi: handler onResize su window mai rimosso.
  5. Fix: cleanup nel useEffect.

Strumenti utili

  • why-is-node-running (Node): elenca timer, sockets, file handle aperti.
  • Chrome Task Manager: tab Shift+Esc mostra memoria per tab.
  • process.memoryUsage() (Node): RSS, heap used/total, external.

Misurare in tempo reale

In browser:

performance.memory  // Chrome-only, deprecato

Usalo solo come spia, non come misura definitiva.

In produzione: integra metriche custom (modulo 9.4) per inviare process.memoryUsage() al backend di monitoring.

Esercizio guidato

  1. Crea una piccola app con un componente che si monta/smonta su click.
  2. Nel componente, registra un setInterval senza cleanup.
  3. Apri Memory panel. Snapshot iniziale.
  4. Monta/smonta il componente 10 volte. Snapshot finale.
  5. Conta gli “Interval handler” o oggetti correlati: il numero cresce.
  6. Aggiungi cleanup. Ripeti. Numero stabile.

Errori frequenti del principiante

  • Cleanup omesso ad ogni addEventListener.
  • Cache senza eviction.
  • Confondere “memoria alta” con leak: a volte è growth normale (heap riservato).
  • Confidare che il GC “farà”. GC libera solo se non c’è più riferimento.