W3docs

Ottimizzazione delle prestazioni nello sviluppo web

Guida alle tecniche di ottimizzazione del DOM in JavaScript: ridurre gli accessi, evitare il layout thrashing e usare requestAnimationFrame.

Il DOM è l'elemento in assoluto più costoso che la maggior parte delle applicazioni JavaScript tocca. Leggere una proprietà come offsetHeight può costringere il browser a fermarsi e ricalcolare la geometria della pagina, mentre scrivere nel DOM può innescare un reflow (ricalcolo di posizioni e dimensioni degli elementi) e un repaint (ridisegno dei pixel). Farlo incautamente all'interno di un ciclo trasforma una pagina che dovrebbe essere istantanea in qualcosa di scattoso.

Questa guida spiega perché le operazioni sul DOM sono lente, per poi mostrare tecniche concrete per risolverlo: ridurre al minimo gli accessi, evitare il layout thrashing, raggruppare le modifiche con requestAnimationFrame e document.createDocumentFragment(), e profilare ciò che non si può intuire.

Perché le operazioni sul DOM sono lente

JavaScript gira su un motore veloce, ma il DOM è il confine tra quel motore e la pipeline di rendering del browser. Attraversare quel confine ripetutamente è ciò che fa male:

  • Reflow (layout) — il browser ricalcola la posizione e la dimensione degli elementi. Un reflow su un elemento può propagarsi ai suoi antenati, discendenti e fratelli.
  • Repaint — il browser ridisegna i pixel (colori, ombre, visibilità) senza modificare la geometria. Meno costoso del reflow, ma non gratuito.

Il punto chiave: il browser cerca di raggruppare queste operazioni per te. Mette in coda le tue scritture e le scarica tutte in una volta, appena prima che il frame successivo venga dipinto. Rompi questa ottimizzazione nel momento in cui leggi una proprietà di layout, perché il browser deve scaricare immediatamente tutte le scritture in sospeso per fornirti una risposta accurata. Questo scaricamento forzato si chiama layout sincrono (forzato), e farlo in un ciclo è la causa principale della maggior parte dei problemi di prestazioni del DOM.

Ridurre al minimo gli accessi al DOM

Ogni lettura e scrittura di una proprietà attraversa il confine JS-DOM, quindi l'ottimizzazione più economica è farne di meno.

  • Memorizzare nella cache i riferimenti agli elementi. Recupera un elemento una volta sola e salvalo in una variabile invece di chiamare document.querySelector a ogni iterazione.
  • Leggere in variabili locali. I contatori dei cicli, le lunghezze e i valori calcolati appartengono a variabili JavaScript, non vanno riletti dal DOM a ogni passaggio.
  • Costruire stringhe, non nodi, quando appropriato. Assegnare innerHTML una volta sola è spesso più veloce che inserire molti nodi uno alla volta — anche se si perdono i listener degli eventi ed è pericoloso con input non affidabile.
// Slow: re-queries and re-reads the DOM on every iteration
for (let i = 0; i < items.length; i++) {
  document.getElementById('list').appendChild(makeRow(items[i]));
}

// Fast: resolve the reference once, outside the loop
const list = document.getElementById('list');
for (let i = 0; i < items.length; i++) {
  list.appendChild(makeRow(items[i]));
}

Vedi Selezionare elementi del DOM per scegliere selettori veloci e specifici — preferisci getElementById e querySelector mirato rispetto a catene discendenti profonde come div > ul li span.

Gestione efficiente degli eventi

Associare un listener a ogni elemento di una lunga lista spreca memoria e rallenta gli aggiornamenti del DOM. La event delegation associa un solo listener a un genitore condiviso e usa event.target per identificare quale figlio è stato colpito, affidandosi al bubbling degli eventi.

// One listener handles the whole list, including rows added later
document.getElementById('list').addEventListener('click', (event) => {
  const row = event.target.closest('li');
  if (row) console.log('clicked row:', row.dataset.id);
});

Questo scala a migliaia di elementi e copre automaticamente gli elementi aggiunti dopo che il listener è stato impostato. Approfondisci in Gestione degli eventi nel DOM e Introduzione agli eventi del browser.

Capire ed evitare il layout thrashing

Cos'è il layout thrashing?

Il layout thrashing si verifica quando si alternano letture e scritture di proprietà di layout in rapida successione. Ogni lettura forza un layout sincrono per scaricare la scrittura precedente, quindi un ciclo di lettura-scrittura-lettura-scrittura innesca un reflow per ogni iterazione invece di uno solo in totale.

// Bad: read (offsetWidth) forces layout, then write invalidates it — every loop
const boxes = document.querySelectorAll('.box');
boxes.forEach((box) => {
  box.style.width = box.offsetWidth + 10 + 'px'; // read + write interleaved
});

Come risolverlo: raggruppare le letture, poi le scritture

Raggruppa prima tutte le letture, poi esegui tutte le scritture. Il browser effettua un solo passaggio di layout per le letture e uno per le scritture.

const boxes = document.querySelectorAll('.box');

// 1. Read phase — collect every measurement first
const widths = [...boxes].map((box) => box.offsetWidth);

// 2. Write phase — now apply all changes; no read interrupts them
boxes.forEach((box, i) => {
  box.style.width = widths[i] + 10 + 'px';
});

Pianificare il lavoro con requestAnimationFrame

requestAnimationFrame esegue il tuo callback appena prima del prossimo repaint, che è il momento ideale per applicare modifiche al DOM — vengono unite in un singolo frame invece di innescare repaint intermedi.

const element = document.getElementById('box');

requestAnimationFrame(() => {
  const width = element.offsetWidth; // read
  element.style.width = width + 10 + 'px'; // write, applied in the same frame
});

Per le animazioni, tieni tutte le scritture al DOM all'interno del callback di requestAnimationFrame e non leggere mai il layout nel mezzo di esse.

Raggruppare le modifiche al DOM

Usare document.createDocumentFragment()

document.createDocumentFragment() è un contenitore leggero fuori dallo schermo. I nodi aggiunti a un fragment non fanno parte del documento attivo, quindi costruirlo non innesca alcun reflow. Quando si aggiunge il fragment completato alla pagina, il browser inserisce tutti i suoi figli in una singola operazione — un reflow invece di uno per nodo.

Esempio

<!DOCTYPE html>
<html>
<head>
    <title>Batching DOM Changes</title>
</head>
<body>
    <div id="container"></div>

    <script>
        const container = document.getElementById('container');
        const fragment = document.createDocumentFragment();

        for (let i = 0; i < 40; i++) {
            const div = document.createElement('div');
            div.textContent = `Item ${i}`;
            fragment.appendChild(div);
        }

        container.appendChild(fragment); // Batch update
    </script>
</body>
</html>

Il ciclo costruisce 40 elementi nel fragment senza alcun impatto sulla pagina visibile. Solo il container.appendChild(fragment) finale tocca il DOM attivo, quindi il browser esegue un singolo passaggio di layout invece di 40. (I motori moderni permettono anche di aggiungere un array di nodi in una sola chiamata con container.append(...nodes), con un effetto di raggruppamento simile.)

Evitare il layout sincrono forzato

Una versione sottile del thrashing è leggere una proprietà di layout subito dopo una modifica di stile, il che costringe il browser a ricalcolare immediatamente il layout:

const box = document.getElementById('box');

box.classList.add('expanded'); // write — queues a layout change
const height = box.offsetHeight; // read — forces layout NOW to answer

Se non hai bisogno del valore immediatamente, rimanda la lettura al frame successivo con requestAnimationFrame, o ristruttura il codice in modo che tutte le letture avvengano prima di qualsiasi scrittura. Le proprietà comuni che innescano il layout forzato quando vengono lette includono offsetTop/offsetWidth/offsetHeight, clientWidth/clientHeight, scrollTop e getComputedStyle().

Best practice

  1. Rimanda il JavaScript non critico. Aggiungi l'attributo defer ai tag <script> in modo che il browser continui a fare il parsing dell'HTML ed esegua lo script dopo che il DOM è pronto. Usa async per script di terze parti indipendenti.
  2. Preferisci i toggle di classe agli stili inline. Cambiare una classe CSS permette al browser di applicare molte regole di stile in un solo reflow, invece di un reflow per ogni assegnazione style inline.
  3. Anima transform e opacity. Queste sono compositate dalla GPU e saltano completamente layout e paint, a differenza dell'animazione di width, top o margin.
  4. Stacca, modifica, reinserisci. Per modifiche pesanti, rimuovi un sottoalbero dal documento (o nascondilo con display: none), modificalo fuori dallo schermo, poi reinseriscilo.
  5. Profila prima di ottimizzare. Usa il pannello Performance dei DevTools del browser per trovare il vero collo di bottiglia invece di fare supposizioni — vedi Debug del DOM e strumenti.
Informazione

La regola d'oro: raggruppa le letture del DOM insieme, poi raggruppa le scritture insieme. Ogni volta che una lettura segue una scrittura, il browser è costretto a ricalcolare il layout in modo sincrono. Raggrupparle gli permette di svolgere il lavoro una volta sola per frame.

Insidie comuni

  • Chiamare document.querySelector all'interno di un ciclo invece di memorizzare il risultato nella cache.
  • Leggere offsetWidth/offsetHeight e scrivere stili nella stessa iterazione del ciclo.
  • Associare un listener di eventi separato a ogni elemento in una lista grande e dinamica invece di usare la delegation.
  • Animare proprietà che attivano il layout (width, left, margin) invece di transform/opacity.

Conclusione

Le prestazioni del DOM si riducono a un'idea: il browser è veloce nel raggruppare il suo lavoro di rendering, e il tuo compito è evitare di rompere quel raggruppamento. Memorizza i riferimenti nella cache, delega gli eventi, raggruppa le letture prima delle scritture, costruisci aggiornamenti grandi all'interno di un DocumentFragment, e pianifica le modifiche visive con requestAnimationFrame. In caso di dubbio, profila — il pannello Performance dei DevTools ti mostrerà esattamente dove si verificano i reflow. Successivamente, approfondisci il tuo repertorio con Manipolazione del DOM e Tecniche avanzate del DOM.

Esercitazione

Pratica
Quali delle seguenti tecniche sono importanti per ottimizzare le prestazioni del DOM?
Quali delle seguenti tecniche sono importanti per ottimizzare le prestazioni del DOM?
Was this page helpful?