Jank busting per prestazioni di rendering migliori

Tom Wiltzius
Tom Wiltzius

Introduzione

Vuoi che la tua app web sia reattiva e fluida quando esegui animazioni, transizioni e altri piccoli effetti dell'interfaccia utente. Assicurarsi che questi effetti non presentino scatti può fare la differenza tra un'esperienza "nativa" e una goffa e poco raffinata.

Questo è il primo di una serie di articoli sull'ottimizzazione delle prestazioni di rendering nel browser. Per iniziare, spiegheremo perché creare animazioni fluide è difficile e cosa occorre fare per riuscirci, oltre a fornire alcune semplici best practice. Molte di queste idee sono state presentate per la prima volta in "Jank Busters", un talk che io e Nat Duca abbiamo tenuto alla conferenza Google I/O di quest'anno (video).

Introduzione alla sincronizzazione verticale

I giocatori PC potrebbero conoscere questo termine, ma non è molto comune sul web: che cos'è la sincronizzazione verticale?

Prendi ad esempio il display del tuo smartphone: si aggiorna a intervalli regolari, in genere (ma non sempre) circa 60 volte al secondo. La sincronizzazione verticale (o V-sync) si riferisce alla pratica di generare nuovi frame solo tra gli aggiornamenti dello schermo. Puoi considerarlo come una condizione di gara tra il processo che scrive i dati nel buffer dello schermo e il sistema operativo che li legge per visualizzarli. Vogliamo che i contenuti dei fotogrammi memorizzati nella cache cambino tra un aggiornamento e l'altro, non durante questi aggiornamenti; altrimenti il monitor mostrerà metà di un fotogramma e metà di un altro, causando "tearing".

Per ottenere un'animazione fluida, devi avere un nuovo frame pronto ogni volta che si verifica un aggiornamento dello schermo. Ciò ha due importanti implicazioni: la temporizzazione dei frame (ovvero il momento in cui il frame deve essere pronto) e il budget dei frame (ovvero il tempo a disposizione del browser per produrre un frame). Hai a disposizione solo il tempo tra un aggiornamento e l'altro dello schermo per completare un fotogramma (~16 ms su uno schermo a 60 Hz) e vuoi iniziare a produrre il fotogramma successivo non appena l'ultimo è stato visualizzato sullo schermo.

Il tempismo è tutto: requestAnimationFrame

Molti sviluppatori web utilizzano setInterval o setTimeout ogni 16 millisecondi per creare animazioni. Si tratta di un problema per diversi motivi (ne parleremo più approfonditamente tra un minuto), ma di particolare preoccupazione sono:

  • La risoluzione del timer da JavaScript è solo dell'ordine di alcuni millisecondi
  • I vari dispositivi hanno frequenze di aggiornamento diverse

Ricorda il problema di temporizzazione dei fotogrammi menzionato sopra: devi avere un fotogramma di animazione completo, completato con eventuali JavaScript, manipolazione DOM, layout, disegno e così via, prima del successivo aggiornamento della schermata. Una risoluzione del timer bassa può rendere difficile completare i frame dell'animazione prima del successivo aggiornamento dello schermo, ma la variazione delle frequenze di aggiornamento dello schermo rende impossibile l'uso di un timer fisso. Indipendentemente dall'intervallo del timer, uscirai lentamente dalla finestra di temporizzazione di un frame e finirai per perderne uno. Questo accadrebbe anche se il timer si attivasse con una precisione di millisecondi, il che non avviene (come scoperto dagli sviluppatori). La risoluzione del timer varia a seconda che il computer sia in funzione a batteria o collegato alla corrente, può essere influenzata dalle schede in background che occupano risorse e così via. Anche se si tratta di un evento raro (ad esempio ogni 16 frame perché hai sbagliato di un millisecondo), noterai che perderai diversi frame al secondo. Dovrai anche generare frame che non vengono mai visualizzati, il che spreca potenza e tempo della CPU che potresti dedicare ad altre attività nella tua applicazione.

I diversi display hanno frequenze di aggiornamento diverse: 60 Hz è una frequenza comune, ma alcuni smartphone hanno 59 Hz, alcuni laptop scendono a 50 Hz in modalità di basso consumo e alcuni monitor da computer sono a 70 Hz.

Quando parliamo di prestazioni di rendering, tendiamo a concentrarci sui frame al secondo (FPS), ma la varianza può essere un problema ancora più grande. I nostri occhi notano i piccoli e irregolari arresti nell'animazione che possono essere prodotti da un'animazione con un tempo inadeguato.

Per ottenere frame di animazione con tempi corretti, utilizza requestAnimationFrame. Quando utilizzi questa API, chiedi al browser un frame di animazione. Il callback viene chiamato quando il browser sta per produrre un nuovo frame. Ciò accade indipendentemente dalla frequenza di aggiornamento.

requestAnimationFrame ha anche altre interessanti proprietà:

  • Le animazioni nelle schede in background vengono messe in pausa, risparmiando risorse di sistema e durata della batteria.
  • Se il sistema non è in grado di gestire il rendering alla frequenza di aggiornamento dello schermo, può limitare le animazioni e produrre il callback meno di frequente (ad esempio 30 volte al secondo su uno schermo a 60 Hz). Anche se dimezza la frequenza dei fotogrammi, mantiene l'animazione coerente e, come affermato sopra, i nostri occhi sono molto più sensibili alla varianza rispetto alla frequenza dei fotogrammi. Una frequenza costante di 30 Hz è migliore di 60 Hz con qualche frame perso al secondo.

requestAnimationFrame è già stato ampiamente discusso, quindi consulta articoli come questo di Creative JS per saperne di più, ma è un primo passo importante per rendere fluida l'animazione.

Budget per i frame

Poiché vogliamo che un nuovo frame sia pronto a ogni aggiornamento dello schermo, abbiamo solo il tempo che intercorre tra un aggiornamento e l'altro per fare tutto il lavoro necessario per creare un nuovo frame. Su un display a 60 Hz, significa che abbiamo circa 16 ms per eseguire tutto il codice JavaScript, eseguire il layout, la pittura e qualsiasi altra operazione che il browser deve eseguire per mostrare il frame. Ciò significa che se l'esecuzione del codice JavaScript all'interno del callback requestAnimationFrame richiede più di 16 ms, non hai alcuna possibilità di produrre un frame in tempo per la sincronizzazione verticale.

16 ms non è molto tempo. Fortunatamente, gli Strumenti per sviluppatori di Chrome possono aiutarti a capire se stai superando il budget di frame durante il callback di requestAnimationFrame.

Se apriamo la sequenza temporale di Dev Tools e acquisiamo una registrazione di questa animazione in azione, vediamo subito che abbiamo superato di gran lunga il budget durante l'animazione. In Spostamenti, passa a "Inquadrature" e dai un'occhiata:

Una demo con troppi layout
Una demo con troppi layout

I callback requestAnimationFrame (rAF) richiedono più di 200 ms. Si tratta di un ordine di grandezza troppo lungo per generare un frame ogni 16 ms. L'apertura di uno di questi lunghi callback rAF rivela cosa succede all'interno: in questo caso, molti layout.

Il video di Paul fornisce maggiori dettagli sulla causa specifica del nuovo layout (viene visualizzato scrollTop) e su come evitarlo. Il punto è che puoi esaminare il callback e capire perché ci vuole così tanto tempo.

Una demo aggiornata con un layout molto ridotto
Una demo aggiornata con un layout molto ridotto

Nota i tempi di frame di 16 ms. Lo spazio vuoto nei frame è lo spazio di manovra che hai per fare di più (o per lasciare che il browser svolga le operazioni necessarie in background). Lo spazio vuoto è un bene.

Altre fonti di problemi

Il problema più grande quando si tenta di eseguire animazioni basate su JavaScript è che altri elementi possono interferire con il callback rAF e persino impedirne l'esecuzione. Anche se il tuo callback rAF è snello ed esegue in pochi millisecondi, altre attività (come l'elaborazione di un XHR appena ricevuto, l'esecuzione di gestori di eventi di input o l'esecuzione di aggiornamenti pianificati su un timer) possono comparire all'improvviso ed essere eseguite per qualsiasi periodo di tempo senza interruzioni. Sui dispositivi mobili, a volte l'elaborazione di questi eventi può richiedere centinaia di millisecondi, durante i quali l'animazione sarà completamente bloccata. Chiamiamo queste esitazioni nell'animazione jank.

Non esiste una soluzione magica per evitare queste situazioni, ma esistono alcune best practice di architettura per prepararti al meglio:

  • Non eseguire troppi calcoli negli elaboratori di input. L'uso di molto codice JS o il tentativo di riorganizzare l'intera pagina durante, ad esempio, un gestore onscroll è una causa molto comune di terribili balzi.
  • Esegui il maggior numero possibile di operazioni di elaborazione (ovvero qualsiasi operazione che richiede molto tempo per l'esecuzione) nel callback rAF o nei worker web.
  • Se inserisci il lavoro nel callback rAF, prova a suddividerlo in modo da elaborare solo un po' di ogni frame o a ritardarlo fino al termine di un'animazione importante. In questo modo, puoi continuare a eseguire callback rAF brevi e animare senza problemi.

Per un ottimo tutorial su come inviare l'elaborazione ai callback di requestAnimationFrame anziché ai gestori di input, consulta l'articolo di Paul Lewis Animazioni più snelle, efficaci e veloci con requestAnimationFrame.

Animazione CSS

Cosa c'è di meglio del codice JS leggero nei callback di eventi e rAF? Nessun JS.

In precedenza abbiamo detto che non esiste una soluzione definitiva per evitare di interrompere i callback rAF, ma puoi utilizzare l'animazione CSS per evitarli del tutto. In particolare su Chrome per Android (e altri browser stanno lavorando a funzionalità simili), le animazioni CSS hanno la proprietà molto interessante che il browser può spesso eseguirle anche se è in esecuzione JavaScript.

Nella sezione precedente sul jitter è presente un'affermazione implicita: i browser possono fare una sola cosa alla volta. Non è strettamente vero, ma è una buona ipotesi di lavoro: in qualsiasi momento il browser può eseguire JavaScript, eseguire il layout o la pittura, ma solo una cosa alla volta. Questo può essere verificato nella visualizzazione Spostamenti di Strumenti per gli sviluppatori. Una delle eccezioni a questa regola sono le animazioni CSS su Chrome per Android (e presto su Chrome per computer, anche se non ancora).

Se possibile, utilizza un'animazione CSS per semplificare l'applicazione e consentire l'esecuzione fluida delle animazioni, anche durante l'esecuzione di JavaScript.

  // see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for info on rAF polyfills
  rAF = window.requestAnimationFrame;

  var degrees = 0;
  function update(timestamp) {
    document.querySelector('#foo').style.webkitTransform = "rotate(" + degrees + "deg)";
    console.log('updated to degrees ' + degrees);
    degrees = degrees + 1;
    rAF(update);
  }
  rAF(update);

Se fai clic sul pulsante, JavaScript viene eseguito per 180 ms, causando un ritardo. Tuttavia, se gestiamo l'animazione con animazioni CSS, il problema non si verifica più.

Tieni presente che, al momento della stesura di questo articolo, le animazioni CSS sono prive di scatti solo su Chrome per Android, non su Chrome per computer.

  /* tools like Modernizr (http://modernizr.com/) can help with CSS polyfills */
  #foo {
    +animation-duration: 3s;
    +animation-timing-function: linear;
    +animation-animation-iteration-count: infinite;
    +animation-animation-name: rotate;
  }

  @+keyframes: rotate; {
    from {
      +transform: rotate(0deg);
    }
    to {
      +transform: rotate(360deg);
    }
  }

Per ulteriori informazioni sull'utilizzo delle animazioni CSS, consulta articoli come questo su MDN.

Riepilogo

In breve:

  1. Quando crei un'animazione, è importante produrre frame per ogni aggiornamento dello schermo. L'animazione con sincronizzazione verticale ha un impatto positivo enorme sull'esperienza utente di un'app.
  2. Il modo migliore per ottenere un'animazione con vsync in Chrome e in altri browser moderni è utilizzare l'animazione CSS. Quando hai bisogno di una maggiore flessibilità rispetto a quella offerta dall'animazione CSS, la tecnica migliore è l'animazione basata su requestAnimationFrame.
  3. Per mantenere le animazioni rAF in salute e felici, assicurati che altri gestori eventi non interferiscano con l'esecuzione del tuo callback rAF e mantieni brevi i callback rAF (<15 ms).

Infine, l'animazione con vsync non si applica solo alle semplici animazioni dell'interfaccia utente, ma anche alle animazioni Canvas2D, alle animazioni WebGL e persino allo scorrimento nelle pagine statiche. Nel prossimo articolo di questa serie approfondiremo il rendimento dello scorrimento tenendo presenti questi concetti.

Buona animazione!

Riferimenti