Migliorare le animazioni della tua web app
TL;DR: Animation Worklet ti consente di scrivere animazioni imperative che vengono eseguite con la frequenza frame nativa del dispositivo per una fluidità extra senza scatti™, rendono le animazioni più resistenti agli scatti del thread principale e sono collegabili allo scorrimento anziché al tempo. Il worklet di animazione è disponibile in Chrome Canary (dietro il flag "Funzionalità della piattaforma web sperimentale") e stiamo pianificando una prova dell'origine per Chrome 71. Puoi iniziare a utilizzarlo come miglioramento progressivo oggi stesso.
Un'altra API Animation?
In realtà no, è un'estensione di ciò che abbiamo già e per una buona ragione. Iniziamo dall'inizio. Se oggi vuoi animare qualsiasi elemento DOM sul web, hai a disposizione due opzioni e mezzo: Transizioni CSS per transizioni semplici da A a B, Animazioni CSS per animazioni basate sul tempo potenzialmente cicliche e più complesse e API Web Animations (WAAPI) per animazioni quasi arbitrariamente complesse. La matrice di supporto di WAAPI sembra piuttosto scoraggiante, ma è in crescita. Fino ad allora, è disponibile un polyfill.
Tutti questi metodi hanno in comune il fatto che sono senza stato e basati sul tempo. Tuttavia, alcuni degli effetti che gli sviluppatori stanno provando non sono basati sul tempo né privi di stato. Ad esempio, l'infame scorrimento con parallasse è, come suggerisce il nome, basato sullo scorrimento. Implementare uno scorrimento con effetto parallasse di alto rendimento sul web oggi è sorprendentemente difficile.
E che dire della mancanza di stato? Ad esempio, la barra degli indirizzi di Chrome su Android. Se scorri verso il basso, non sarà più visibile. Ma al primo scorrimento verso l'alto, torna indietro, anche se sei a metà della pagina. L'animazione dipende non solo dalla posizione di scorrimento, ma anche dalla direzione di scorrimento precedente. È stateful.
Un altro problema riguarda lo stile delle barre di scorrimento. Sono notoriamente difficili da acconciare o almeno non abbastanza. Cosa succede se voglio una micio di Nyan come barra di scorrimento? Qualunque tecnica tu scelga, creare una barra di scorrimento personalizzata non è né facile né efficiente.
Il punto è che tutte queste cose sono complicate e difficili, se non impossibili, da implementare in modo efficiente. La maggior parte si basa su eventi e/o
requestAnimationFrame
, che potrebbero mantenere i 60 fps, anche se lo schermo è capable di funzionare a 90 fps, 120 fps o più e utilizzare una frazione del
prezioso budget del frame del thread principale.
Il worklet di animazione estende le funzionalità della pila di animazioni del web per semplificare questo tipo di effetti. Prima di iniziare, assicurati di avere un aggiornamento sulle nozioni di base delle animazioni.
Introduzione ad animazioni e schemi temporali
WAAPI e Animation Worklet fanno ampio uso delle sequenze temporali per consentirti di orchestrare animazioni ed effetti nel modo che preferisci. Questa sezione è un breve ripasso o un'introduzione alle sequenze temporali e al loro funzionamento con le animazioni.
Ogni documento ha document.timeline
. Inizia da 0 quando il documento viene creato e conteggia i millisecondi da quando è stato creato. Tutte le animazioni di un documento funzionano in base a questa sequenza temporale.
Per rendere le cose un po' più concrete, diamo un'occhiata a questo snippet WAAPI
const animation = new Animation(
new KeyframeEffect(
document.querySelector('#a'),
[
{
transform: 'translateX(0)',
},
{
transform: 'translateX(500px)',
},
{
transform: 'translateY(500px)',
},
],
{
delay: 3000,
duration: 2000,
iterations: 3,
}
),
document.timeline
);
animation.play();
Quando chiamiamo animation.play()
, l'animazione utilizza currentTime
della sequenza temporale come ora di inizio. La nostra animazione ha un ritardo di 3000 ms, il che significa che inizierà (o diventerà "attiva") quando la sequenza temporale raggiunge "startTime
- 3000
. After that time, the animation engine will animate the given element from the first keyframe (
translateX(0)), through all intermediate keyframes (
translateX(500px)) all the way to the last keyframe (
translateY(500px)) in exactly 2000ms, as prescribed by the
durationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline's
currentTimeis
startTime + 3000 + 1000and the last keyframe at
startTime + 3000 + 2000`. Il punto è che la sequenza temporale controlla dove siamo nell'animazione.
Una volta raggiunta l'ultima keyframe, l'animazione tornerà al primo
keyframe e avvierà l'iterazione successiva dell'animazione. Questo processo si ripete
per un totale di 3 volte poiché abbiamo impostato iterations: 3
. Se volessimo che l'animazione non si interrompesse mai, scriveremo iterations: Number.POSITIVE_INFINITY
. Ecco il
risultato del codice
sopra.
WAAPI è incredibilmente potente e include molte altre funzionalità, come easing, offset di inizio, ponderazioni delle keyframe e comportamento di riempimento, che esulano dall'ambito di questo articolo. Per saperne di più, ti consiglio di leggere questo articolo sulle animazioni CSS su CSS Tricks.
Scrivere un worklet di animazione
Ora che abbiamo compreso il concetto di schemi temporali, possiamo iniziare a esaminare il worklet di animazione e come ti consente di modificare gli schemi temporali. L'API Animation Worklet non si basa solo su WAAPI, ma è, nel senso del web estensibile, una primitiva di livello inferiore che spiega come funziona WAAPI. In termini di sintassi, sono incredibilmente simili:
Worklet di animazione | WAAPI |
---|---|
new WorkletAnimation( 'passthrough', new KeyframeEffect( document.querySelector('#a'), [ { transform: 'translateX(0)' }, { transform: 'translateX(500px)' } ], { duration: 2000, iterations: Number.POSITIVE_INFINITY } ), document.timeline ).play(); |
new Animation( new KeyframeEffect( document.querySelector('#a'), [ { transform: 'translateX(0)' }, { transform: 'translateX(500px)' } ], { duration: 2000, iterations: Number.POSITIVE_INFINITY } ), document.timeline ).play(); |
La differenza sta nel primo parametro, ovvero il nome del worklet che gestisce questa animazione.
Rilevamento di funzionalità
Chrome è il primo browser a implementare questa funzionalità, quindi devi assicurarti che il tuo codice non preveda semplicemente la presenza di AnimationWorklet
. Pertanto, prima di caricare il worklet, dobbiamo rilevare se il browser dell'utente supportaAnimationWorklet
con un semplice controllo:
if ('animationWorklet' in CSS) {
// AnimationWorklet is supported!
}
Caricamento di un worklet
I worklet sono un nuovo concetto introdotto dal team Houdini per semplificare la creazione e la scalabilità di molte delle nuove API. Tratteremo i dettagli dei worklet un po' più avanti, ma per semplicità puoi considerarli per il momento come thread economici e leggeri (come i worker).
Prima di dichiarare l'animazione, dobbiamo assicurarci di aver caricato un worklet denominato "passthrough":
// index.html
await CSS.animationWorklet.addModule('passthrough-aw.js');
// ... WorkletAnimation initialization from above ...
// passthrough-aw.js
registerAnimator(
'passthrough',
class {
animate(currentTime, effect) {
effect.localTime = currentTime;
}
}
);
Che cosa accade in questo caso? Stiamo registrando un corso come animatore utilizzando la chiamata registerAnimator()
di AnimationWorklet, assegnandogli il nome "passthrough".
È lo stesso nome utilizzato nel costruttore WorkletAnimation()
sopra. Al termine della registrazione, la promessa restituita da addModule()
verrà risolta e potremo iniziare a creare animazioni utilizzando il worklet.
Il metodo animate()
della nostra istanza verrà chiamato per ogni frame che il browser vuole eseguire il rendering, passando il currentTime
della sequenza temporale dell'animazione nonché l'effetto attualmente in fase di elaborazione. Abbiamo un solo effetto, KeyframeEffect
, e utilizziamo currentTime
per impostare il valore localTime
dell'effetto, motivo per cui questo animatore si chiama "passthrough". Con questo codice per il worklet, WAAPI e AnimationWorklet sopra si comportano esattamente allo stesso modo, come puoi vedere nella demo.
Ora
Il parametro currentTime
del nostro metodo animate()
è il currentTime
della
sequenza temporale che abbiamo passato al costruttore WorkletAnimation()
. Nell'esempio precedente abbiamo semplicemente passato questo tempo all'effetto. Ma poiché si tratta di codice JavaScript, possiamo distorcere il tempo 💫
function remap(minIn, maxIn, minOut, maxOut, v) {
return ((v - minIn) / (maxIn - minIn)) * (maxOut - minOut) + minOut;
}
registerAnimator(
'sin',
class {
animate(currentTime, effect) {
effect.localTime = remap(
-1,
1,
0,
2000,
Math.sin((currentTime * 2 * Math.PI) / 2000)
);
}
}
);
Prendiamo il Math.sin()
del currentTime
e rimappiamo il valore all'intervallo [0; 2000], che è l'intervallo di tempo per cui è definito il nostro effetto. Ora
l'animazione ha un aspetto molto diverso, senza dover
modificare i fotogrammi chiave o le opzioni dell'animazione. Il codice del worklet può essere
arbitrariamente complesso e consente di definire in modo programmatico quali effetti devono essere
riprodotti, in quale ordine e in che misura.
Opzioni su Opzioni
Potresti voler riutilizzare un worklet e modificarne i numeri. Per questo motivo, il costruttore WorkletAnimation consente di passare un oggetto opzioni al worklet:
registerAnimator(
'factor',
class {
constructor(options = {}) {
this.factor = options.factor || 1;
}
animate(currentTime, effect) {
effect.localTime = currentTime * this.factor;
}
}
);
new WorkletAnimation(
'factor',
new KeyframeEffect(
document.querySelector('#b'),
[
/* ... same keyframes as before ... */
],
{
duration: 2000,
iterations: Number.POSITIVE_INFINITY,
}
),
document.timeline,
{factor: 0.5}
).play();
In questo esempio, entrambe le animazioni sono basate sullo stesso codice, ma con opzioni diverse.
Dammi il tuo stato locale.
Come accennato in precedenza, uno dei problemi principali che il worklet di animazione si propone di risolvere è rappresentato dalle animazioni con stato. I worklet di animazione possono mantenere lo stato. Tuttavia, una
delle funzionalità di base dei worklet è che è possibile eseguirne la migrazione a un altro
thread o addirittura distruggerli per risparmiare risorse, il che ne distruggerebbe anche
lo stato. Per evitare la perdita dello stato, il worklet di animazione offre un hook chiamato prima dell'eliminazione di un worklet che puoi utilizzare per restituire un oggetto stato. Questo oggetto verrà passato al costruttore quando il worklet viene nuovamente creato. Al momento della creazione iniziale, il parametro sarà undefined
.
registerAnimator(
'randomspin',
class {
constructor(options = {}, state = {}) {
this.direction = state.direction || (Math.random() > 0.5 ? 1 : -1);
}
animate(currentTime, effect) {
// Some math to make sure that `localTime` is always > 0.
effect.localTime = 2000 + this.direction * (currentTime % 2000);
}
destroy() {
return {
direction: this.direction,
};
}
}
);
Ogni volta che aggiorni questa demo, hai una probabilità di 50/50
in merito alla direzione in cui ruoterà il quadrato. Se il browser dovesse smontare il worklet e eseguirne la migrazione in un altro thread, verrà eseguita un'altra chiamata Math.random()
al momento della creazione, il che potrebbe causare un improvviso cambiamento di direzione. Per evitare che ciò accada, restituiamo la direzione scelta in modo casuale come state e la utilizziamo nel costruttore, se fornito.
Collegamento al continuum spazio-temporale: ScrollTimeline
Come mostrato nella sezione precedente, AnimationWorklet ci consente di
definire in modo programmatico in che modo l'avanzamento della sequenza temporale influisce sugli effetti dell'animazione. Finora, però, la nostra sequenza temporale è sempre stata document.timeline
, che monitora il tempo.
ScrollTimeline
apre nuove possibilità e ti consente di gestire le animazioni con lo scorrimento anziché con il tempo. Riutilizzeremo il nostro primo
worklet "passthrough" per questa
demo:
new WorkletAnimation(
'passthrough',
new KeyframeEffect(
document.querySelector('#a'),
[
{
transform: 'translateX(0)',
},
{
transform: 'translateX(500px)',
},
],
{
duration: 2000,
fill: 'both',
}
),
new ScrollTimeline({
scrollSource: document.querySelector('main'),
orientation: 'vertical', // "horizontal" or "vertical".
timeRange: 2000,
})
).play();
Anziché passare document.timeline
, stiamo creando un nuovo ScrollTimeline
.
Come avrai intuito, ScrollTimeline
non utilizza il tempo, ma la posizione di scorrimento di scrollSource
per impostare currentTime
nel worklet. Se scorri fino in alto (o a sinistra), il valore è currentTime = 0
, mentre se scorri fino in basso (o a destra), il valore currentTime
diventa timeRange
. Se scorri la casella in questa
demo, puoi controllare la posizione della casella rossa.
Se crei un ScrollTimeline
con un elemento che non scorre, il ScrollTimeline
della sequenza temporale sarà NaN
.currentTime
Quindi, in particolare con il design adattabile in mente, devi sempre essere preparato per NaN
come currentTime
. Spesso è sensato impostare un valore predefinito pari a 0.
Il collegamento delle animazioni alla posizione dello scorrimento è qualcosa che è stato cercato a lungo, ma non è mai stato raggiunto a questo livello di fedeltà (a parte alcune soluzioni alternative con CSS3D). Animation Worklet consente di implementare questi effetti in modo semplice e con un rendimento elevato. Ad esempio, un effetto di scorrimento parallattico come questo demo mostra che ora bastano un paio di righe per definire un'animazione basata sullo scorrimento.
dietro le quinte
Worklet
I worklet sono contesti JavaScript con un ambito isolato e una superficie API molto piccola. La piccola interfaccia API consente un'ottimizzazione più aggressiva dal browser, in particolare sui dispositivi di fascia bassa. Inoltre, i worklet non sono associati a un loop di eventi specifico, ma possono essere spostati da un thread all'altro in base alle esigenze. Questo è particolarmente importante per AnimationWorklet.
Compositor NSync
Potresti sapere che alcune proprietà CSS sono rapide da animare, mentre altre no. Alcune proprietà richiedono solo un po' di lavoro sulla GPU per essere animate, mentre altre obligano il browser a riorganizzare l'intero documento.
In Chrome (come in molti altri browser) abbiamo un processo chiamato compositore, čijo je radno mjesto - i ovdje jako pojednostavljujem - da svrsta slojeve i teksture i potom iskoristi GPU za ažuriranje ekrana što je moguće redovnije, idealno onoliko brzo koliko ekran može da se ažurira (obično 60 Hz). A seconda delle proprietà CSS animate, il browser potrebbe dover semplicemente lasciare che sia il compositore a svolgere il proprio lavoro, mentre altre proprietà devono eseguire il layout, un'operazione che solo il thread principale può eseguire. A seconda delle proprietà che prevedi di animare, il worklet di animazione verrà associato al thread principale o eseguito in un thread separato in sincronia con il compositore.
Colpire il polso
In genere esiste un solo processo di compositore potenzialmente condiviso tra più schede, poiché la GPU è una risorsa molto contesa. Se il compositore viene bloccato in qualche modo, l'intero browser si blocca e non risponde più agli input dell'utente. Questo deve essere evitato a tutti i costi. Che cosa succede se il worklet non riesce a fornire i dati di cui il compositore ha bisogno in tempo per il rendering del frame?
In questo caso, il worklet è consentito, in base alle specifiche, a "sfuggire". Rallenta rispetto al compositore, che può riutilizzare i dati dell'ultimo frame per mantenere alto il frame rate. Visivamente, il risultato sarà simile a un effetto jitter, ma la grande differenza è che il browser è ancora reattivo all'input dell'utente.
Conclusione
AnimationWorklet offre molti vantaggi al web. I vantaggi evidenti sono un maggiore controllo sulle animazioni e nuovi modi per animarle al fine di offrire un nuovo livello di fedeltà visiva al web. Tuttavia, il design delle API consente anche di rendere l'app più resiliente ai problemi di aggiornamento, garantendo al contempo l'accesso a tutte le nuove funzionalità.
Il worklet di animazione è nella versione Canary e puntiamo a una prova dell'origine con Chrome 71. Non vediamo l'ora di scoprire le tue nuove esperienze web e di sapere cosa possiamo migliorare. Esiste anche un polyfill che fornisce la stessa API, ma non l'isolamento delle prestazioni.
Tieni presente che le transizioni CSS e le animazioni CSS sono ancora opzioni valide e possono essere molto più semplici per le animazioni di base. Ma se hai bisogno di qualcosa di più elaborato, AnimationWorklet è la soluzione che fa per te.