Perdita di memoria da finestre scollegate

Trova e correggi le perdite di memoria complesse causate da finestre scollegate.

Bartek Nowierski
Bartek Nowierski

Che cos'è una perdita di memoria in JavaScript?

Una perdita di memoria è un aumento involontario della quantità di memoria utilizzata da un'applicazione nel tempo. In JavaScript, le perdite di memoria si verificano quando gli oggetti non sono più necessari, ma vengono comunque richiamati da funzioni o altri oggetti. Questi riferimenti impediscono che gli oggetti non necessari vengano recuperati dal garbage collector.

Il compito del garbage collector è identificare e recuperare gli oggetti che non sono più raggiungibili dall'applicazione. Questo funziona anche quando gli oggetti fanno riferimento a se stessi o si fanno riferimento tra loro in modo ciclico. Una volta che non ci sono più riferimenti tramite i quali un'applicazione potrebbe accedere a un gruppo di oggetti, è possibile eseguire la raccolta dei rifiuti.

let A = {};
console.log(A); // local variable reference

let B = {A}; // B.A is a second reference to A

A = null; // unset local variable reference

console.log(B.A); // A can still be referenced by B

B.A = null; // unset B's reference to A

// No references to A are left. It can be garbage collected.

Una classe di perdite di memoria particolarmente insidiosa si verifica quando un'applicazione fa riferimento a oggetti che hanno il proprio ciclo di vita, come elementi DOM o finestre popup. È possibile che questi tipi di oggetti diventino inutilizzati senza che l'applicazione lo sappia, il che significa che il codice dell'applicazione potrebbe avere gli unici riferimenti rimanenti a un oggetto che altrimenti potrebbe essere sottoposto a garbage collection.

Che cos'è una finestra staccata?

Nell'esempio seguente, un'applicazione di visualizzazione di presentazioni include pulsanti per aprire e chiudere un popup di note del presentatore. Immagina che un utente faccia clic su Mostra note, quindi chiuda direttamente la finestra popup invece di fare clic sul pulsante Nascondi note: la variabile notesWindow contiene ancora un riferimento al popup a cui è possibile accedere, anche se il popup non è più in uso.

<button id="show">Show Notes</button>
<button id="hide">Hide Notes</button>
<script type="module">
  let notesWindow;
  document.getElementById('show').onclick = () => {
    notesWindow = window.open('/presenter-notes.html');
  };
  document.getElementById('hide').onclick = () => {
    if (notesWindow) notesWindow.close();
  };
</script>

Questo è un esempio di finestra staccata. La finestra popup è stata chiusa, ma il nostro codice contiene un riferimento che impedisce al browser di distruggerla e recuperare la memoria.

Quando una pagina chiama window.open() per creare una nuova finestra o scheda del browser, viene restituito un oggetto Window che rappresenta la finestra o la scheda. Anche dopo che una finestra di questo tipo è stata chiusa o l'utente è uscito, l'oggetto Window restituito da window.open() può essere ancora utilizzato per accedere alle informazioni su di essa. Questo è un tipo di finestra scollegata: poiché il codice JavaScript può ancora potenzialmente accedere alle proprietà dell'oggetto Window chiuso, deve essere mantenuto in memoria. Se la finestra includeva molti oggetti o iframe JavaScript, la memoria non può essere recuperata finché non rimangono riferimenti JavaScript alle proprietà della finestra.

Utilizzo di Chrome DevTools per dimostrare come è possibile conservare un documento dopo la chiusura di una finestra.

Lo stesso problema può verificarsi anche quando si utilizzano elementi <iframe>. Gli iframe si comportano come finestre nidificate che contengono documenti e la loro proprietà contentWindow fornisce l'accesso all'oggetto Window contenuto, in modo molto simile al valore restituito da window.open(). Il codice JavaScript può mantenere un riferimento a contentWindow o contentDocument di un iframe anche se l'iframe viene rimosso dal DOM o se il suo URL cambia, il che impedisce la raccolta dei rifiuti del documento poiché è ancora possibile accedere alle sue proprietà.

Demo di come un gestore degli eventi può conservare il documento di un iframe, anche dopo aver eseguito la navigazione nell'iframe a un URL diverso.

Nei casi in cui un riferimento a document all'interno di una finestra o di un iframe viene mantenuto da JavaScript, il documento verrà mantenuto in memoria anche se la finestra o l'iframe contenente si apre su un nuovo URL. Questo può essere particolarmente problematico quando il codice JavaScript che contiene il riferimento non rileva che la finestra/il frame ha eseguito la navigazione a un nuovo URL, poiché non sa quando diventa l'ultimo riferimento che mantiene un documento in memoria.

In che modo le finestre scollegate causano perdite di memoria

Quando si utilizzano finestre e iframe nello stesso dominio della pagina principale, è normale ascoltare eventi o accedere alle proprietà oltre i confini del documento. Ad esempio, esaminiamo di nuovo una variante dell'esempio di visualizzatore di presentazioni all'inizio di questa guida. Il visualizzatore apre una seconda finestra per la visualizzazione delle note del relatore. La finestra delle note del relatore ascolta gli eventiclick come segnale per passare alla diapositiva successiva. Se l'utente chiude questa finestra delle note, il codice JavaScript in esecuzione nella finestra principale originale ha ancora accesso completo al documento delle note dell'altoparlante:

<button id="notes">Show Presenter Notes</button>
<script type="module">
  let notesWindow;
  function showNotes() {
    notesWindow = window.open('/presenter-notes.html');
    notesWindow.document.addEventListener('click', nextSlide);
  }
  document.getElementById('notes').onclick = showNotes;

  let slide = 1;
  function nextSlide() {
    slide += 1;
    notesWindow.document.title = `Slide  ${slide}`;
  }
  document.body.onclick = nextSlide;
</script>

Immaginiamo di chiudere la finestra del browser creata da showNotes() sopra. Non esiste un gestore di eventi che ascolti per rilevare la chiusura della finestra, pertanto non viene comunicato al codice di eliminare i riferimenti al documento. La funzione nextSlide() è ancora "attiva" perché è associata come gestore dei clic nella nostra pagina principale e il fatto che nextSlide contenga un riferimento a notesWindow significa che la finestra è ancora indicata e non può essere sottoposta a garbage collection.

Illustrazione di come i riferimenti a una finestra ne impediscano la raccolta dei rifiuti una volta chiusa.

Esistono diversi altri scenari in cui i riferimenti vengono conservati per errore e impediscono alle finestre scollegate di essere idonee per la raccolta dei rifiuti:

  • I gestori di eventi possono essere registrati nel documento iniziale di un iframe prima che il frame acceda all'URL previsto, con il risultato che i riferimenti accidentali al documento e all'iframe rimangono dopo la rimozione di altri riferimenti.

  • Un documento che richiede molta memoria caricato in una finestra o in un iframe può essere mantenuto in memoria per errore molto tempo dopo aver eseguito la navigazione a un nuovo URL. Questo accade spesso perché la pagina principale conserva i riferimenti al documento per consentire la rimozione dell'ascoltatore.

  • Quando passi un oggetto JavaScript a un'altra finestra o a un iframe, la catena del prototipo dell'oggetto include riferimenti all'ambiente in cui è stato creato, inclusa la finestra che lo ha creato. Ciò significa che è importante evitare di conservare riferimenti a oggetti di altre finestre quanto evitare di conservare riferimenti alle finestre stesse.

    index.html:

    <script>
      let currentFiles;
      function load(files) {
        // this retains the popup:
        currentFiles = files;
      }
      window.open('upload.html');
    </script>
    

    upload.html:

    <input type="file" id="file" />
    <script>
      file.onchange = () => {
        parent.load(file.files);
      };
    </script>
    

Rilevamento di perdite di memoria causate da finestre scollegate

Individuare le perdite di memoria può essere complicato. Spesso è difficile creare riproduzioni isolate di questi problemi, in particolare quando sono coinvolti più documenti o finestre. Per complicare ulteriormente la situazione, l'ispezione di potenziali riferimenti con perdite può finire per creare riferimenti aggiuntivi che impediscono la raccolta dei rifiuti degli oggetti ispezionati. A tal fine, è utile iniziare con strumenti che evitano specificamente di introdurre questa possibilità.

Un ottimo punto di partenza per eseguire il debug dei problemi di memoria è acquisire uno snapshot dell'heap. Ciò fornisce una visualizzazione istantanea della memoria attualmente utilizzata da un'applicazione, ovvero di tutti gli oggetti che sono stati creati ma non ancora sottoposti a garbage collection. Gli snapshot dell'heap contengono informazioni utili sugli oggetti, incluse le dimensioni e un elenco delle variabili e delle chiusure che fanno riferimento a questi oggetti.