Le animazioni sono diventate parte del linguaggio di molti siti: menu che scorrono, schede che si espandono, elementi che compaiono mentre si scorre la pagina, pulsanti che reagiscono al passaggio del mouse, contatori, caroselli, effetti di parallasse. Ben fatte, rendono l’interfaccia più comprensibile e piacevole. Fatte male, rendono la pagina lenta, fanno scattare lo scorrimento e peggiorano le Core Web Vitals, in particolare l’INP e, in alcuni casi, il CLS.
La differenza tra un’animazione fluida e una che rallenta tutto sta quasi sempre in un dettaglio tecnico: quali proprietà vengono animate e da chi. In questo articolo vediamo cosa fa il browser a ogni fotogramma, perché alcune proprietà costano molto meno di altre, come usare correttamente il livello di composizione, come gestire le animazioni legate allo scorrimento e come verificare il risultato.
Sedici millisecondi per fotogramma
La maggior parte degli schermi si aggiorna sessanta volte al secondo. Perché un’animazione appaia fluida, il browser deve produrre un nuovo fotogramma a ogni aggiornamento, cioè ogni 16,7 millisecondi circa, e in quel tempo deve fare tutto: eseguire eventuali script, ricalcolare gli stili, aggiornare il layout, ridisegnare e comporre la pagina. Se un fotogramma richiede più tempo, viene saltato, e l’animazione procede a scatti. Sugli schermi con frequenze più alte, come 90 o 120 aggiornamenti al secondo, il tempo a disposizione è ancora minore.
Come abbiamo visto nell’articolo sul percorso critico di rendering, per mostrare una pagina il browser passa per diverse fasi: calcolo degli stili, layout, disegno, composizione. Un’animazione le ripete a ogni fotogramma, ma non sempre tutte: dipende da cosa cambia.
Le proprietà costose e quelle economiche
Le proprietà che influenzano la geometria degli elementi, come width, height, top, left, margin e padding, costringono il browser a ricalcolare il layout: la posizione e la dimensione dell’elemento animato, ma spesso anche di tutti gli elementi che lo circondano. Poi deve ridisegnare le parti cambiate e comporre il risultato. È il percorso più costoso, e su pagine complesse può superare da solo il tempo di un fotogramma.
Le proprietà che cambiano solo l’aspetto, come i colori, gli sfondi e le ombre, non toccano il layout ma richiedono di ridisegnare i pixel dell’elemento. Il costo è intermedio, e cresce con la superficie ridisegnata: un’ombra ampia animata su un elemento grande può essere sorprendentemente pesante.
Infine transform e opacity. Spostare, ridimensionare, ruotare o rendere trasparente un elemento con queste proprietà non cambia il layout e non richiede di ridisegnarlo: se l’elemento si trova su un proprio livello di composizione, il browser prende l’immagine già disegnata e la sposta, la scala o ne cambia la trasparenza, spesso con l’aiuto della scheda grafica. È il motivo per cui la regola più importante per animazioni fluide è animare solo transform e opacity, ogni volta che è possibile.
/* Da evitare: il pannello scorre modificando left, ricalcolo del layout a ogni fotogramma */
.pannello { position: fixed; left: -320px; transition: left 0.3s; }
.pannello.aperto { left: 0; }
/* Meglio: lo stesso effetto con transform, solo composizione */
.pannello { position: fixed; left: 0; transform: translateX( -100% ); transition: transform 0.3s; }
.pannello.aperto { transform: translateX( 0 ); }
Il compositor e il main thread
C’è un secondo vantaggio, meno noto ma ancora più importante. Le animazioni CSS di transform e opacity possono essere eseguite interamente dal compositor, un componente del browser che lavora su un thread separato dal main thread. Questo significa che continuano a essere fluide anche quando il main thread è occupato dal JavaScript: un menu che si apre, un indicatore di caricamento che gira, una transizione tra schede non scattano anche se nel frattempo la pagina sta eseguendo uno script pesante.
Le animazioni delle proprietà geometriche, invece, dipendono dal main thread: se questo è occupato, i fotogrammi aspettano. Ed è vero anche il contrario: un’animazione costosa occupa il main thread a ogni fotogramma, lasciando meno tempo per rispondere alle interazioni dell’utente e peggiorando l’INP.
will-change e i livelli
Perché un elemento possa essere animato dal compositor, deve trovarsi su un proprio livello. Il browser crea i livelli automaticamente quando un’animazione di transform o opacity inizia, ma la creazione ha un costo, e può produrre un piccolo scatto al primo fotogramma. La proprietà will-change permette di avvisare il browser in anticipo, così che prepari il livello prima che serva.
Va usata con parsimonia. Ogni livello occupa memoria, spesso nella memoria grafica, e una pagina con centinaia di elementi su livelli separati può diventare più lenta, non più veloce, soprattutto sugli smartphone. will-change ha senso sugli elementi che verranno effettivamente animati a breve, come un menu che sta per aprirsi, e andrebbe rimosso quando l’animazione è terminata. Applicarlo in modo generico a tutti gli elementi, come a volte si vede nei fogli di stile, è controproducente.
Animazioni e CLS
Le animazioni hanno un legame anche con il CLS. Un elemento che cambia posizione nel layout, per esempio perché viene animata la sua altezza o il suo margine, sposta gli elementi che lo seguono, e questi spostamenti possono essere conteggiati come spostamenti inattesi. Le animazioni realizzate con transform, invece, non cambiano il layout e non producono spostamenti conteggiati dal CLS, anche quando l’elemento si muove visivamente. È un ulteriore motivo per preferirle: un banner che scende dall’alto con transform non sposta il resto della pagina, uno che cresce in altezza sì.
Un caso frequente è l’espansione di un contenuto, come una domanda frequente che si apre o una descrizione che si allunga: l’altezza deve cambiare, e il contenuto sottostante deve spostarsi. Se l’espansione avviene in risposta a un clic dell’utente, gli spostamenti che seguono entro breve tempo non vengono considerati inattesi e non pesano sul CLS. Il problema si pone per le animazioni che partono da sole, al caricamento o dopo un certo tempo.
Le animazioni legate allo scorrimento
Gli effetti che si attivano durante lo scorrimento della pagina, come elementi che compaiono, barre di avanzamento della lettura, parallasse, sono tra i più diffusi e tra i più problematici. Tradizionalmente si realizzano ascoltando l’evento scroll in JavaScript e aggiornando gli stili a ogni evento: un lavoro sul main thread che si ripete decine di volte al secondo mentre l’utente scorre, e che rende lo scorrimento stesso meno fluido.
Esistono alternative migliori. Per far comparire gli elementi quando entrano nello schermo, l’API Intersection Observer notifica il JavaScript solo quando un elemento attraversa una soglia, senza calcoli a ogni evento di scorrimento. Per gli effetti continui, i browser moderni supportano le animazioni guidate dallo scorrimento direttamente in CSS, che collegano l’avanzamento di un’animazione alla posizione della pagina e, per le proprietà adatte, vengono eseguite fuori dal main thread.
/* Barra di avanzamento della lettura, senza JavaScript */
.avanzamento {
position: fixed;
inset: 0 0 auto 0;
height: 4px;
background: #174ea6;
transform-origin: 0 50%;
animation: cresci linear;
animation-timeline: scroll( root );
}
@keyframes cresci {
from { transform: scaleX( 0 ); }
to { transform: scaleX( 1 ); }
}
Quando si usa comunque un gestore di scorrimento in JavaScript, conviene dichiararlo passivo, così che il browser non debba attenderne l’esecuzione prima di scorrere, e limitare gli aggiornamenti a un fotogramma alla volta con requestAnimationFrame.
Il layout forzato
Un errore frequente nelle animazioni realizzate in JavaScript è alternare letture e scritture delle proprietà geometriche: leggere l’altezza di un elemento, modificarne un altro, leggere la posizione di un terzo, e così via. Ogni lettura dopo una modifica costringe il browser a ricalcolare subito il layout per restituire un valore aggiornato. In un ciclo su molti elementi, il layout viene ricalcolato decine di volte nello stesso fotogramma. La soluzione è raggruppare prima tutte le letture e poi tutte le scritture, o affidarsi a librerie di animazione che lo fanno automaticamente.
Movimento ridotto
Molti sistemi operativi permettono all’utente di chiedere di ridurre il movimento, un’impostazione importante per chi soffre di disturbi vestibolari. Il sito la legge con la media query prefers-reduced-motion e può eliminare o semplificare le animazioni non essenziali. È una scelta di accessibilità che ha anche un effetto sulle prestazioni: meno animazioni, meno lavoro per il dispositivo.
Transizioni tra pagine e tra stati
Le View Transitions permettono di animare il passaggio da uno stato all’altro della pagina, o da una pagina all’altra, con poche righe di codice: il browser cattura un’immagine dello stato precedente e di quello successivo e le anima con opacity e transform, cioè con le proprietà economiche. È un modo per ottenere l’effetto fluido delle applicazioni a pagina singola senza la loro complessità. Va però ricordato che durante la transizione la pagina è in attesa: transizioni lunghe allungano il tempo percepito prima che il nuovo contenuto sia utilizzabile. Durate brevi, intorno ai 200-300 millisecondi, sono di solito sufficienti.
Librerie, temi e costruttori di pagine
Molte animazioni non vengono scritte a mano ma arrivano da un tema, da un costruttore di pagine o da una libreria. Vale la pena verificare cosa animano: alcuni effetti di comparsa usano ancora top o margin, alcuni caroselli spostano i contenuti modificando left, alcuni effetti di parallasse aggiornano la posizione dello sfondo a ogni evento di scorrimento. Le librerie moderne di animazione usano transform per impostazione predefinita, ma lasciano libertà di animare qualunque proprietà. Inoltre ogni libreria ha un peso, e caricarne una intera per far comparire tre riquadri è un costo sproporzionato, già discusso a proposito del costo dei costruttori di pagine: spesso qualche riga di CSS e un Intersection Observer bastano.
Un esempio concreto
Consideriamo un sito aziendale con un menu laterale per dispositivi mobili, animato modificando la proprietà left, e una serie di riquadri che compaiono durante lo scorrimento grazie a un gestore dell’evento scroll che modifica margin-top e opacity. Sugli smartphone di fascia media lo scorrimento scatta visibilmente e l’apertura del menu è irregolare, soprattutto nei primi secondi, quando il main thread è ancora occupato dagli script. Riscrivere il menu con transform, sostituire il gestore di scorrimento con un Intersection Observer che aggiunge una classe, e animare la comparsa con transform e opacity elimina i fotogrammi saltati, riduce il lavoro del main thread durante lo scorrimento e toglie dal CLS gli spostamenti causati dalla variazione del margine. Un intervento di poche ore, con effetti visibili a ogni visita.
Come verificare
I DevTools di Chrome offrono diversi strumenti. Nel pannello Rendering si possono evidenziare le aree ridisegnate, che lampeggiano a ogni disegno, e i bordi dei livelli di composizione: un’animazione che fa lampeggiare ampie aree della pagina sta ridisegnando troppo. Il pannello Performance mostra i fotogrammi, evidenziando quelli saltati, e per ciascuno il lavoro svolto: ricalcolo degli stili, layout, disegno, script. Registrando con il rallentamento della CPU attivo si vede come si comporta l’animazione su un dispositivo di fascia media.
Lighthouse segnala le animazioni che non possono essere eseguite dal compositor, indicando la proprietà responsabile. È un buon punto di partenza per trovare le animazioni da riscrivere, soprattutto nei temi e nei costruttori di pagine che offrono effetti già pronti.
In sintesi
Un’animazione fluida deve produrre un fotogramma ogni 16,7 millisecondi. Le proprietà geometriche costringono il browser a ricalcolare il layout e ridisegnare a ogni fotogramma, occupano il main thread e possono generare spostamenti conteggiati dal CLS; transform e opacity richiedono solo la composizione, possono essere eseguite dal compositor anche quando il main thread è occupato e non spostano il layout.
Le regole pratiche sono poche: animare transform e opacity, usare will-change solo dove serve e per il tempo necessario, sostituire i gestori di scorrimento con Intersection Observer o con le animazioni guidate dallo scorrimento, evitare letture e scritture alternate delle proprietà geometriche, rispettare la preferenza di movimento ridotto. Il risultato è un’interfaccia più piacevole e, allo stesso tempo, più rapida a rispondere.