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 di UI. Assicurarsi che questi effetti siano privi di jank può fare la differenza tra un o un po' goffo e non rifinito.

È il primo di una serie di articoli che trattano l'ottimizzazione delle prestazioni di rendering nel browser. Per iniziare, spiegheremo il motivo per cui un'animazione fluida è difficile e cosa occorre fare per raggiungerla, oltre ad alcune semplici best practice. Molte di queste idee sono state presentate in origine in "Jank Busters", io e Nat Duca abbiamo tenuto al Google I/O talk (video) quest'anno.

Introduzione a V-sync

I giocatori di PC conoscono questo termine, ma è raro sul Web: che cos'è v-sync?

Considera il display del tuo smartphone: si aggiorna a intervalli regolari, di solito (ma non sempre) circa 60 volte al secondo. La sincronizzazione V (o sincronizzazione verticale) si riferisce alla pratica di generare nuovi frame solo tra gli aggiornamenti dello schermo. Si potrebbe considerare come una condizione di gara tra il processo che scrive i dati nel buffer dello schermo e il sistema operativo che li legge per mostrarli sul display. L'obiettivo è che i contenuti del frame inserito nel buffer vengano modificati tra questi aggiornamenti, non durante l'aggiornamento. altrimenti il monitor mostrerà metà di un frame e metà di un altro, con "tealing".

Per ottenere un'animazione fluida è necessario che sia pronto un nuovo frame ogni volta che viene aggiornato lo schermo. Questo comporta due grandi implicazioni: la durata del frame (ovvero il momento in cui il frame deve essere pronto entro il giorno) e il budget del frame (ossia il tempo che il browser ha per produrre un frame). Tra l'intervallo di tempo tra gli aggiornamenti dello schermo e l'esecuzione di un frame (circa 16 ms su una schermata a 60 Hz) è possibile iniziare a produrre il frame successivo non appena l'ultimo è stato visualizzato sullo schermo.

La tempistica è tutto: requestAnimationFrame

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

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

Ricorda il problema di durata dei frame sopra menzionato: è necessario un frame di animazione completato, terminato con qualsiasi JavaScript, manipolazione del DOM, layout, disegno e così via, in modo da essere pronto prima che si verifichi il successivo aggiornamento dello schermo. Una risoluzione del timer bassa può rendere difficile il completamento dei fotogrammi dell'animazione prima dell'aggiornamento successivo dello schermo, ma la variazione della frequenza di aggiornamento dello schermo non è possibile con un timer fisso. A prescindere dall'intervallo del timer, uscirai lentamente dalla finestra di tempo per un frame e finirai per farne cadere uno. Ciò accadrebbe anche se il timer si avviava con una precisione al millisecondo, cosa che non sarebbe successo (come hanno scoperto gli sviluppatori): la risoluzione del timer varia a seconda che la macchina sia alimentata o collegata all'alimentazione, può essere influenzata da risorse di manipolazione delle schede in background e così via. Anche se si tratta di un evento raro (ad esempio, ogni 16 fotogrammi perché l'audio era spento di un millisecondo) noterai che perdi diversi frame al secondo. Inoltre, ti dedicherai a generare frame che non vengono mai visualizzati, il che comporta uno spreco di energia e tempo di CPU che potresti dedicare ad altre attività nella tua applicazione.

La frequenza di aggiornamento varia a seconda del display: 60 Hz è comune, ma alcuni telefoni 59 Hz, alcuni laptop scendeno a 50 Hz in modalità a basso consumo, alcuni monitor di desktop sono a 70 Hz.

Tendiamo a concentrarci sui frame al secondo (f/s) quando parliamo delle prestazioni del rendering, ma la varianza può essere un problema ancora più grande. I nostri occhi notano i piccoli intoppi irregolari nell'animazione che sono in grado di produrre in un'animazione con tempi di attesa scadenti.

Per ottenere correttamente i frame dell'animazione sincronizzati, utilizza requestAnimationFrame. Quando utilizzi quest'API, chiedi al browser un frame di animazione. Il callback viene chiamato quando il browser produrrà presto un nuovo frame. Ciò 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 questa frequenza dimezza i fotogrammi, mantiene l'animazione coerente e, come già detto, i nostri occhi sono molto più attenti alla varianza che alla frequenza fotogrammi. Una frequenza costante di 30 Hz sembra migliore di 60 Hz che salta qualche frame al secondo.

Abbiamo già parlato di requestAnimationFrame, quindi puoi fare riferimento ad articoli come questo articolo di Creative JS per saperne di più, ma si tratta di un primo passo importante per rendere più fluida l'animazione.

Budget frame

Dato che vogliamo che sia pronto un nuovo frame a ogni aggiornamento della schermata, tra un aggiornamento e l'altro passa solo il tempo 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, colorare e qualsiasi altra cosa che il browser faccia per rimuovere il frame. Ciò significa che se l'esecuzione del codice JavaScript all'interno del callback requestAnimationFrame richiede più di 16 ms, non hai alcuna speranza di produrre un frame in tempo per v-sync.

16 ms non sono molto tempo. Fortunatamente, gli Strumenti per sviluppatori di Chrome possono aiutarti a controllare se stai esaurendo il budget del tuo frame durante la callback requestAnimationFrame.

L'apertura della sequenza temporale degli Strumenti per sviluppatori e la registrazione di questa animazione in azione mostrano rapidamente che abbiamo superato il budget per l'animazione. Nella sequenza temporale, passa a "Frame". e dai un'occhiata:

Una demo con un layout troppo elevato
Una demo con un layout troppo eccessivo

I callback requestAnimationFrame (rAF) stanno richiedendo più di 200 ms. Si tratta di un ordine di grandezza troppo lungo per spuntare un fotogramma ogni 16 ms. L'apertura di uno di questi lunghi callback rAF rivela cosa sta succedendo: in questo caso, molti layout.

Il video di Paolo descrive in modo più dettagliato la causa specifica del relayout (indica scrollTop) e come evitarlo. Il punto, però, è che puoi analizzare il 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

Nota la durata frame di 16 ms. Questo spazio vuoto nei frame è il margine necessario per svolgere più attività (o lasciare che sia il browser a occuparsi del lavoro in background). Quello spazio vuoto è una buona cosa.

Altra fonte di Jank

La causa principale dei problemi si verifica quando si tenta di eseguire animazioni basate su JavaScript è che altre cose possono intralciare il tuo callback rAF e persino impedirle l'esecuzione. Anche se il callback rAF è magro e viene eseguito in poche 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) può improvvisamente entrare e funzionare per qualsiasi periodo di tempo senza cedere. Su dispositivo mobile a volte l'elaborazione di questi eventi richiede centinaia di millisecondi, durante i quali l'animazione sarà completamente bloccata. Le chiamiamo l'animazione segnala jank.

Non esiste una formula magica per evitare queste situazioni, ma esistono alcune best practice sull'architettura per ottenere risultati ottimali:

  • Non eseguire molte elaborazioni nei gestori di input. Uso elevato di codice JS o tentativo di riorganizzare l'intera pagina, ad es. Un gestore onscroll è una causa molto comune di una grave incolumità.
  • Esegui il push del maggior tempo possibile di elaborazione (lettura: tutto ciò che richiede molto tempo per essere eseguito) al callback rAF o ai web worker.
  • Se inoltri 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 potrai continuare a eseguire brevi callback rAF e animare senza problemi.

Per un tutorial eccezionale 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 JS leggero nei tuoi eventi e callback rAF? Nessun codice JS.

In precedenza abbiamo detto che non esiste una formula magica per evitare di interrompere i callback rAF, ma è possibile utilizzare l'animazione CSS per evitare del tutto. In particolare, in Chrome per Android (e in altri browser funzionano funzionalità simili), le animazioni CSS hanno la caratteristica più desiderabile che il browser possa spesso eseguirle anche se JavaScript è in esecuzione.

Nella sezione precedente relativa a jank è presente un'affermazione implicita: i browser possono fare una sola cosa alla volta. Questo non è strettamente vero, ma è un buon presupposto: in un dato momento il browser può eseguire JS, eseguire il layout o la pittura, ma solo uno alla volta. Puoi verificarlo nella visualizzazione Sequenza temporale degli strumenti di sviluppo. Una delle eccezioni a questa regola sono le animazioni CSS su Chrome per Android (e presto su Chrome per desktop, anche se non ancora).

Se possibile, l'utilizzo di un'animazione CSS semplifica l'applicazione e consente l'esecuzione delle animazioni in modo fluido, 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. Tuttavia, se invece provvediamo a guidare l'animazione con animazioni CSS, il jank non si verifica più.

Tieni presente che al momento della stesura del presente documento, l'animazione CSS non presenta problemi su Chrome per Android, non su Chrome per 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 articolo su MDN.

Riepilogo

In breve:

  1. Durante l'animazione, la produzione di frame per ogni aggiornamento dello schermo è importante. L'animazione Vsync ha un enorme impatto positivo sull'aspetto dell'app.
  2. Il modo migliore per ottenere l'animazione vsync in Chrome e in altri browser moderni è per utilizzare l'animazione CSS. Quando hai bisogno di maggiore flessibilità rispetto all'animazione CSS la tecnica migliore è l'animazione basata su requestAnimationFrame.
  3. Per mantenere l'integrità e la soddisfazione delle animazioni rAF, assicurati che gli altri gestori di eventi non stanno intralciando l'esecuzione del callback rAF e mantieni le callback rAF breve (< 15 ms).

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

Buon divertimento con l'animazione!

Riferimenti