Un'animazione che scatta dà subito l'impressione di un sito lento, anche se la pagina si è caricata in fretta. Per apparire fluida, un'animazione deve produrre un nuovo fotogramma a ogni aggiornamento dello schermo: su un display a 60 Hz significa circa 16 millisecondi per fotogramma, e una parte di quel tempo serve al browser per il proprio lavoro. Se il codice o il CSS richiedono più tempo, il fotogramma salta e l'occhio lo nota.
Capire perché succede richiede di conoscere le fasi con cui il browser produce un'immagine.
Le fasi del rendering
Per ogni fotogramma il browser può dover eseguire, in ordine:
- JavaScript: gestori di evento, timer, callback di animazione;
- stile: calcolo delle regole CSS applicate a ciascun elemento;
- layout: posizione e dimensione di ogni elemento;
- paint: disegno dei pixel di testi, colori, bordi e ombre in uno o più livelli;
- composite: assemblaggio dei livelli nell'immagine finale, spesso con l'aiuto della GPU.
Il punto chiave è che non tutte le modifiche innescano tutte le fasi. Cambiare width o top richiede di rifare il layout, poi il paint, poi la composizione. Cambiare background-color salta il layout ma richiede il paint. Cambiare transform o opacity su un elemento che ha un proprio livello richiede solo la composizione, che può avvenire su un thread separato dal main thread.
Animare le proprietà giuste
| Proprietà animata | Fasi coinvolte | Fluidità |
|---|---|---|
transform, opacity |
Solo composizione | Ottima |
color, background-color, box-shadow |
Paint e composizione | Dipende dall'area |
width, height, top, left, margin |
Layout, paint, composizione | Spesso scarsa |
Quasi tutti gli effetti comuni si possono ottenere con le proprietà della prima riga:
- uno spostamento si fa con
transform: translate()invece che conleftotop; - un ingrandimento con
transform: scale()invece che conwidtheheight; - una dissolvenza con
opacity; - un pannello che si apre può partire da
transform: translateY(-100%)invece di animare l'altezza.
Animare transform ha anche un vantaggio per il CLS: gli spostamenti ottenuti con le trasformazioni non vengono conteggiati come spostamenti di layout.
will-change e i livelli
Per animare un elemento solo in composizione, il browser lo deve collocare su un livello proprio. Spesso lo fa da solo all'avvio dell'animazione, ma la creazione del livello può costare un fotogramma lento proprio all'inizio. La proprietà will-change lo avvisa in anticipo:
.cassetto {
will-change: transform;
}
È uno strumento da usare con parsimonia. Ogni livello occupa memoria della GPU, e applicare will-change a decine di elementi, o peggio a *, può peggiorare le prestazioni invece di migliorarle. Va riservato agli elementi che si animano davvero e spesso, oppure aggiunto poco prima dell'animazione e rimosso al termine.
Il layout thrashing
Il problema più subdolo non riguarda il CSS ma il JavaScript. Normalmente il browser accumula le modifiche e ricalcola il layout una volta sola prima di disegnare. Ma se il codice legge una proprietà geometrica, come offsetHeight, getBoundingClientRect() o scrollTop, subito dopo aver modificato qualcosa, il browser è costretto a ricalcolare il layout in quel momento per restituire un valore corretto. È il reflow forzato.
Se questo accade dentro un ciclo, il layout viene ricalcolato a ogni iterazione:
// Da evitare: lettura e scrittura alternate
for (const box of scatole) {
box.style.height = (box.offsetWidth / 2) + 'px';
}
La correzione consiste nel separare le letture dalle scritture:
// Meglio: prima tutte le letture, poi tutte le scritture
const larghezze = scatole.map(b => b.offsetWidth);
scatole.forEach((b, i) => { b.style.height = (larghezze[i] / 2) + 'px'; });
Con molti elementi la differenza è enorme: da centinaia di ricalcoli a uno solo.
requestAnimationFrame
Per le animazioni guidate da JavaScript, requestAnimationFrame esegue il codice appena prima del fotogramma successivo, sincronizzato con lo schermo. È preferibile a setTimeout o setInterval, che possono cadere in un momento qualsiasi del fotogramma e produrre scatti. Inoltre il browser sospende le callback quando la scheda non è visibile, risparmiando batteria.
Per molti effetti, però, la scelta migliore resta il CSS: transizioni e animazioni dichiarate nel foglio di stile su transform e opacity possono girare sul thread del compositor anche quando il main thread è occupato. La Web Animations API offre lo stesso vantaggio con il controllo di JavaScript.
Verificare con DevTools
Chrome DevTools offre strumenti specifici, nel pannello Rendering (dal menu dei comandi):
- Paint flashing evidenzia in verde le aree ridisegnate: se durante un'animazione lampeggia tutta la pagina, qualcosa non va;
- Layout Shift Regions mostra le aree che si spostano;
- Frame Rendering Stats visualizza in tempo reale i fotogrammi persi.
Nel pannello Performance, i reflow forzati vengono segnalati con un avviso sulle voci di layout, e il riquadro di dettaglio indica la riga di codice che li ha provocati. È il modo più rapido per trovare il ciclo responsabile.
In sintesi
- Un'animazione fluida deve stare nel budget di un fotogramma, circa 16 ms a 60 Hz.
- Anima
transformeopacity, che richiedono solo la composizione. - Usa
will-changesolo sugli elementi che si animano davvero. - Evita il layout thrashing separando letture e scritture del DOM.
- Verifica con Paint flashing e con gli avvisi di reflow forzato nel pannello Performance.