Jank busting per prestazioni di rendering migliori

Tom Wiltzius
Tom Wiltzius

Introduzione

Vuoi che la tua app web sia reattiva e fluida durante l'esecuzione di animazioni, transizioni e altri piccoli effetti dell'interfaccia utente. Assicurarsi che questi effetti siano privi di jank può fare la differenza tra un'esperienza "nativa" o un'esperienza goffa e non rifinita.

Questo è il primo di una serie di articoli sull'ottimizzazione delle prestazioni di rendering nel browser. Per iniziare, vedremo perché è difficile ottenere un'animazione fluida e cosa è necessario fare per ottenerla, oltre ad alcune semplici best practice. Molte di queste idee sono state presentate originariamente in "Jank Busters", un intervento che Nat Duca e io abbiamo tenuto quest'anno alla conferenza Google I/O (video).

Introduzione alla sincronizzazione verticale

I gamer su PC potrebbero avere familiarità con questo termine, ma non è comune sul web: che cos'è il v-sync?

Pensa al display del tuo smartphone: si aggiorna a intervalli regolari, di solito (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 pensare a questa situazione come a una race condition tra il processo che scrive i dati nel buffer dello schermo e il sistema operativo che legge i dati per visualizzarli sul display. Vogliamo che i contenuti dei frame memorizzati nel buffer cambino tra questi aggiornamenti, non durante, altrimenti il monitor mostrerà metà di un frame e metà di un altro, causando il "tearing".

Per ottenere un'animazione fluida, è necessario che un nuovo frame sia pronto ogni volta che si verifica un aggiornamento dello schermo. Ciò ha due implicazioni importanti: la durata frame (ovvero quando il frame deve essere pronto) e il budget frame (ovvero il tempo a disposizione del browser per produrre un frame). Hai solo il tempo tra gli aggiornamenti dello schermo per completare un frame (~16 ms su uno schermo a 60 Hz) e vuoi iniziare a produrre il frame successivo non appena l'ultimo viene visualizzato sullo schermo.

Il tempismo è tutto: requestAnimationFrame

Molti sviluppatori web utilizzano setInterval o setTimeout ogni 16 millisecondi per creare animazioni. Questo è un problema per una serie di motivi (di cui parleremo tra un minuto), ma di particolare interesse sono:

  • La risoluzione del timer da JavaScript è solo nell'ordine di diversi millisecondi
  • Dispositivi diversi hanno frequenze di aggiornamento diverse

Ricorda il problema di durata frame menzionato sopra: devi avere un frame di animazione completo, con JavaScript, manipolazione del DOM, layout, disegno e così via, pronto prima del successivo aggiornamento dello schermo. Una bassa risoluzione del timer può rendere difficile il completamento dei frame di animazione prima del successivo aggiornamento dello schermo, ma la variazione delle frequenze di aggiornamento dello schermo lo rende impossibile con un timer fisso. Indipendentemente dall'intervallo del timer, uscirai lentamente dalla finestra di temporizzazione per un frame e finirai per perderne uno. Ciò accadrebbe anche se il timer venisse attivato con una precisione al millisecondo, cosa che non accadrà (come hanno scoperto gli sviluppatori): la risoluzione del timer varia a seconda che la macchina sia alimentata a batteria o collegata alla presa di corrente, può essere influenzata dalle schede in background che monopolizzano le risorse e così via. Anche se questo è raro (ad esempio, ogni 16 frame perché hai perso un millisecondo), lo noterai: perderai diversi frame al secondo. Inoltre, l'app genererà frame che non verranno mai visualizzati, sprecando energia e tempo di CPU che potresti dedicare ad altre attività nell'applicazione.

Display diversi hanno frequenze di aggiornamento diverse: 60 Hz è comune, ma alcuni smartphone hanno 59 Hz, alcuni laptop scendono a 50 Hz in modalità a basso consumo, alcuni monitor desktop hanno 70 Hz.

Quando si parla 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 intoppi irregolari nell'animazione che un'animazione con un timing scadente può produrre.

Il modo per ottenere frame di animazione con tempi corretti è utilizzare 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. Questo accade indipendentemente dalla frequenza di aggiornamento.

requestAnimationFrame ha anche altre proprietà interessanti:

  • 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 con minore frequenza (ad esempio, 30 volte al secondo su uno schermo a 60 Hz). Anche se questo dimezza la frequenza fotogrammi, mantiene l'animazione coerente e, come indicato sopra, i nostri occhi sono molto più sensibili alla variazione che alla frequenza fotogrammi. Una frequenza costante di 30 Hz è migliore di una frequenza di 60 Hz che perde alcuni frame al secondo.

requestAnimationFrame è già stato ampiamente discusso, quindi consulta articoli come questo di Creative JS per ulteriori informazioni, ma è un primo passo importante per un'animazione fluida.

Budget dei frame

Poiché vogliamo che un nuovo frame sia pronto a ogni aggiornamento dello schermo, il tempo a disposizione per creare un nuovo frame è solo quello tra un aggiornamento e l'altro. Su un display a 60 Hz, ciò significa che abbiamo circa 16 ms per eseguire tutto il codice JavaScript, eseguire il layout, il paint e tutto ciò che il browser deve fare per visualizzare il frame. Ciò significa che se il codice JavaScript all'interno del callback requestAnimationFrame impiega più di 16 ms per essere eseguito, non hai alcuna speranza di produrre un frame in tempo per la sincronizzazione verticale.

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

Se apri la cronologia di DevTools e registri questa animazione in azione, vedrai subito che il budget viene superato di molto durante l'animazione. In Timeline, passa a "Frame" e dai un'occhiata:

Una demo con un layout eccessivo
Una demo con un layout eccessivo

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. Se apri uno di questi lunghi callback rAF, vedrai cosa sta succedendo all'interno: in questo caso, molti layout.

Il video di Paul spiega in modo più dettagliato la causa specifica del ricalcolo del layout (la lettura di scrollTop) e come evitarlo. Ma il punto è che puoi approfondire il callback e indagare su cosa sta richiedendo così tanto tempo.

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

Nota le durate frame di 16 ms. Lo spazio vuoto nei frame è il margine di manovra che hai per eseguire più lavoro (o per consentire al browser di eseguire il lavoro necessario in background). Questo spazio vuoto è un aspetto positivo.

Altra fonte di jank

La causa principale dei problemi quando si tenta di eseguire animazioni basate su JavaScript è che altri elementi possono ostacolare il callback rAF e persino impedirne l'esecuzione. Anche se il callback rAF è leggero e viene eseguito in pochi millisecondi, altre attività (come l'elaborazione di un XHR appena arrivato, l'esecuzione di gestori di eventi di input o l'esecuzione di aggiornamenti pianificati su un timer) possono arrivare improvvisamente ed essere eseguite per un periodo di tempo qualsiasi senza interruzioni. Sui dispositivi mobili, a volte l'elaborazione di questi eventi può richiedere centinaia di millisecondi, durante i quali l'animazione si blocca completamente. Questi problemi di animazione sono chiamati jank.

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

  • Non eseguire molte elaborazioni nei gestori di input. L'esecuzione di molti JS o il tentativo di riorganizzare l'intera pagina, ad esempio durante un gestore onscroll, è una causa molto comune di un jank terribile.
  • Sposta il maggior numero possibile di elaborazioni (ovvero tutto ciò che richiede molto tempo per l'esecuzione) nel callback rAF o nei Web Worker.
  • Se inserisci del lavoro nel callback rAF, prova a suddividerlo in modo da elaborare solo una piccola parte di ogni frame o ritardalo fino al termine di un'animazione importante. In questo modo, puoi continuare a eseguire brevi callback rAF e animare in modo fluido.

Per un ottimo tutorial su come trasferire l'elaborazione nei callback requestAnimationFrame anziché nei gestori di input, consulta l'articolo di Paul Lewis Leaner, Meaner, Faster Animations with requestAnimationFrame.

Animazione CSS

Cosa c'è di meglio di un JS leggero nei callback di eventi e rAF? No JS.

In precedenza abbiamo detto che non esiste una soluzione magica per evitare di interrompere i callback rAF, ma puoi utilizzare l'animazione CSS per evitare del tutto la necessità di utilizzarli. 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 JavaScript è in esecuzione.

Nella sezione precedente sul jank è presente un'affermazione implicita: i browser possono fare una sola cosa alla volta. Questo non è del tutto vero, ma è una buona ipotesi di lavoro: in un dato momento il browser può eseguire JS, eseguire il layout o il painting, ma solo uno alla volta. Puoi verificarlo nella visualizzazione cronologica di DevTools. Una delle eccezioni a questa regola sono le animazioni CSS su Chrome per Android (e presto anche su Chrome per computer, anche se non ancora).

Quando possibile, l'utilizzo di un'animazione CSS semplifica l'applicazione e consente alle animazioni di essere eseguite senza problemi, 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 jank. Se invece gestiamo l'animazione con le animazioni CSS, il jank non si verifica più.

(Ricorda che al momento della stesura di questo articolo, l'animazione CSS è priva di jank 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 saperne di più sull'utilizzo delle animazioni CSS, consulta articoli come questo su MDN.

Wrapup

In breve:

  1. Durante l'animazione, è importante produrre frame per ogni aggiornamento dello schermo. L'animazione sincronizzata con Vsync ha un impatto positivo enorme sull'esperienza d'uso di un'app.
  2. Il modo migliore per ottenere un'animazione sincronizzata con la frequenza di aggiornamento in Chrome e in altri browser moderni è utilizzare l'animazione CSS. Quando hai bisogno di una flessibilità maggiore di quella offerta dall'animazione CSS, la tecnica migliore è l'animazione basata su requestAnimationFrame.
  3. Per mantenere le animazioni rAF integre e funzionanti, assicurati che altri gestori di eventi non ostacolino l'esecuzione del callback rAF e mantieni i callback rAF brevi (<15 ms).

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

Buona animazione!

Riferimenti