Jank busting per prestazioni di rendering migliori

Tom Wiltzius
Tom Wiltzius

Introduzione

Vuoi che la tua app web sia reattiva e fluida durante le animazioni, le transizioni e altri piccoli effetti dell'interfaccia utente. Fare in modo che questi effetti siano privi di deboli fare la differenza tra un tono "nativo" o uno grezzo e grezzo.

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

Introduzione alla funzionalità V-sync

I giocatori su PC potrebbero conoscere questo termine, ma è raro sul web: che cos'è v-sync?

Considera il display del tuo telefono: si aggiorna a intervalli regolari, di solito (ma non sempre) circa 60 volte al secondo. La sincronizzazione verticale (o sincronizzazione verticale) si riferisce alla generazione di nuovi frame solo tra gli aggiornamenti dello schermo. Questo potrebbe essere considerato come una condizione di gara tra il processo di scrittura dei dati nel buffer dello schermo e il sistema operativo che li legge per mostrarli sul display. Vogliamo che i contenuti del frame nel buffer cambino durante questi aggiornamenti, non durante gli aggiornamenti; in caso contrario, il monitor mostrerà metà di un frame e metà di un altro, con il risultato di un "tearing".

Per ottenere un'animazione fluida, devi avere un nuovo frame che sia pronto ogni volta che viene eseguito un aggiornamento dello schermo. Questo comporta due importanti implicazioni: la tempistica dei frame (ovvero quando il frame deve essere pronto) e il budget dei frame (ovvero quanto tempo il browser ha a disposizione per produrre un frame). Tra gli aggiornamenti dello schermo c'è solo il tempo che intercorre per completare un fotogramma (circa 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 diversi motivi (di cui parleremo più in dettaglio tra poco), ma di particolare preoccupazione sono:

  • La risoluzione del timer da JavaScript è dell'ordine di alcuni millisecondi
  • Ogni dispositivo ha frequenze di aggiornamento diverse

Ricorda il problema relativo alla durata frame indicato sopra: devi avere un frame dell'animazione completo, completato con JavaScript, manipolazione DOM, layout, disegno e così via, per prepararti prima del successivo aggiornamento della schermata. Una bassa risoluzione del timer può rendere difficile il completamento dei fotogrammi dell'animazione prima dell'aggiornamento successivo della schermata, ma la variazione delle frequenze di aggiornamento della schermata lo rende impossibile con un timer fisso. Indipendentemente dall'intervallo di timer, uscirai lentamente dalla finestra di temporizzazione di un frame e finirai per perderne uno. Ciò si verifica anche se il timer viene attivato con una precisione di pochi millisecondi, cosa che non (come scoperto dagli sviluppatori): la risoluzione del timer varia a seconda che la macchina sia alimentata o meno a batteria, può essere influenzata da schede in background che occupano risorse e così via. Anche se è raro (ad esempio ogni 16 frame perché eri spento di un millisecondo), noterai che vengono persi diversi frame al secondo. Ti impegnerai anche per generare frame che non vengono mai visualizzati, sprecando energia e tempo di CPU che potresti dedicare ad altre cose nella tua applicazione.

Ogni display ha una frequenza di aggiornamento diversa: 60 Hz è comune, ma alcuni telefoni sono a 59 Hz, alcuni laptop scendono a 50 Hz in modalità a basso consumo, alcuni monitor desktop sono 70 Hz.

Tendiamo a concentrarci sui frame al secondo (f/s) quando parliamo delle prestazioni del rendering, ma la varianza può rappresentare un problema ancora maggiore. I nostri occhi notano i piccoli e irregolari attacchi dell'animazione che possono produrre un'animazione con un timestamp errato.

Per ottenere correttamente i fotogrammi dell'animazione con timestamp, requestAnimationFrame. Quando utilizzi questa API, chiedi al browser un frame di animazione. Il callback viene chiamato quando il browser genererà presto un nuovo frame. Ciò avviene indipendentemente dalla frequenza di aggiornamento.

requestAnimationFrame ha anche altre utili 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 frequentemente (ad esempio, 30 volte al secondo su uno schermo a 60 Hz). Anche se la frequenza fotogrammi viene dimezzata, mantiene l'animazione coerente e, come abbiamo detto in precedenza, i nostri occhi sono molto più in sintonia con la varianza rispetto alla frequenza fotogrammi. Una frequenza di 30 Hz costante ha un aspetto migliore di una frequenza di 60 Hz che perde qualche fotogramma al secondo.

requestAnimationFrame è già stato discusso ovunque, quindi fai riferimento ad articoli come questo di codice della creatività JS per maggiori informazioni, ma è un primo passaggio importante per ottimizzare l'animazione.

Budget frame

Poiché vogliamo un nuovo frame pronto a ogni aggiornamento dello schermo, c'è solo il tempo tra un aggiornamento e l'altro per fare tutto il lavoro e creare un nuovo frame. Su un display a 60 Hz, ciò significa che abbiamo circa 16 ms per eseguire tutto il codice JavaScript, eseguire il layout, colorare e qualsiasi altra operazione che deve fare il browser per rimuovere il frame. Questo significa che se l'esecuzione del codice JavaScript all'interno del callback requestAnimationFrame richiede più di 16 ms, non puoi produrre un frame in tempo per v-sync.

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

L'apertura della sequenza temporale degli strumenti per sviluppatori e la registrazione di una registrazione di questa animazione in azione indica rapidamente che abbiamo superato il budget disponibile durante l'animazione. In Spostamenti, passa a "Frame" e dai un'occhiata:

Una demo con layout troppo eccessivo
Una demo con layout eccessivo

I callback requestAnimationFrame (rAF) impiegano più di 200 ms. È un ordine di grandezza troppo lungo per spuntare un frame ogni 16 ms. L'apertura di uno di questi lunghi callback rAF rivela cosa sta succedendo all'interno: in questo caso, molti layout.

Il video di Paul fornisce maggiori dettagli sulla causa specifica del relayout (si tratta di scrollTop) e su come evitarlo. Il punto è che puoi immergerti nel callback e capire cosa sta richiedendo così tanto tempo.

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

Osserva le durate dei frame di 16 ms. Quello spazio vuoto nei frame è il margine che hai per lavorare di più (o lasciare che sia il browser a funzionare in background). Quello spazio vuoto è una cosa bella.

Altra fonte di Jank

Il problema principale dei problemi di esecuzione di animazioni basate su JavaScript è che altri elementi possono ostacolare il callback rAF e addirittura impedirne l'esecuzione. Anche se il callback rAF è magro 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 improvvisamente entrare ed eseguire per qualsiasi periodo di tempo senza arrendersi. Sui dispositivi mobili, a volte l'elaborazione di questi eventi può richiedere centinaia di millisecondi, durante i quali l'animazione risulterà completamente bloccata. Questi ganci di animazione sono chiamati jank.

Non esiste un punto elenco magico per evitare queste situazioni, ma esistono alcune best practice relative all'architettura per ottenere risultati ottimali:

  • Non eseguire molte elaborazioni nei gestori di input. Utilizzare molto codice JS o provare a riorganizzare l'intera pagina durante, ad esempio, un gestore onscroll è una causa molto comune di terribile insufficienza.
  • Introduci il maggior numero possibile di processi di elaborazione (ovvero qualsiasi operazione che richieda molto tempo) nel callback rAF o nei web worker.
  • Se spingi il lavoro nel callback rAF, prova a suddividerlo in blocchi in modo da elaborare solo un po' ogni frame o ritardarlo fino al termine di un'animazione importante. In questo modo puoi continuare a eseguire brevi callback rAF e animarlo in modo fluido.

Per un ottimo tutorial che illustra come eseguire il push dell'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 codice JS leggero nei callback eventi e rAF? Nessun codice JS.

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

Esiste un'istruzione implicita nella sezione precedente su jank: i browser possono fare solo una cosa alla volta. Questo non è strettamente vero, ma è una buona ipotesi di lavoro perché in un determinato momento il browser può eseguire JS, eseguire il layout o eseguire il disegno, ma solo uno alla volta. Questa operazione può essere verificata nella visualizzazione cronologica di Strumenti per 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, l'utilizzo di un'animazione CSS semplifica l'applicazione e consente un'esecuzione ottimale 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 jank. Se invece gestiamo l'animazione con animazioni CSS, il jank non si verifica più.

(Al momento della stesura di questo articolo, ricorda che l'animazione CSS è priva di jank solo su Chrome per Android, non su Chrome per computer desktop.)

  /* 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. Durante l'animazione, è importante produrre frame per ogni aggiornamento dello schermo. L'animazione vsync ha un enorme impatto positivo sull'aspetto di un'app.
  2. Il modo migliore per ottenere l'animazione vsync in Chrome e in altri browser moderni è utilizzare l'animazione CSS. Se hai bisogno di una maggiore flessibilità rispetto a quella offerta dall'animazione CSS, la tecnica migliore è quella basata su requestAnimationFrame.
  3. Per mantenere le animazioni rAF integre e soddisfacenti, assicurati che altri gestori di eventi non ostacolino l'esecuzione del callback rAF e mantieni i callback rAF brevi (< 15 ms).

Infine, l'animazione vsync'd non si applica solo alle semplici animazioni dell'interfaccia utente, ma anche a quelle di Canvas2D, alle animazioni WebGL e allo scorrimento delle pagine statiche. Nel prossimo articolo di questa serie, vedremo nel dettaglio il rendimento dello scorrimento tenendo a mente questi concetti.

Divertiti con l'animazione.

Riferimenti