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. Cominciamo 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 l'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. Ogni volta che scorri verso l'alto, invece, torna indietro, anche se sei a metà pagina. L'animazione dipende non solo dalla posizione di scorrimento, ma anche dalla direzione di scorrimento precedente. È stateful.
Un altro problema è l'applicazione di stili alle barre di scorrimento. Sono notoriamente non stilizzabili o per lo meno 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 potrebbe mantenere i 60 fps, anche quando lo schermo è
in grado 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.
Un'introduzione ad animazioni e sequenze 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 relazione 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
come ora di inizio della sequenza temporale. 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 inizierà 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 riportato sopra.
WAAPI è incredibilmente potente e offre molte altre funzionalità in questa API, come l'easing, gli offset di avvio, le ponderazioni dei fotogrammi chiave e il comportamento di riempimento, che potrebbero sorprendere l'ambito di questo articolo. Per saperne di più, ti consigliamo di leggere questo articolo sulle animazioni CSS sui trucchi CSS.
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 estremamente 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 delle caratteristiche
Chrome è il primo browser a implementare questa funzionalità, quindi devi assicurarti che il tuo codice non preveda solo la presenza di AnimationWorklet
. Quindi, prima di caricare il
worklet, dobbiamo rilevare se il browser dell'utente supporta
AnimationWorklet
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).
Dobbiamo assicurarci di aver caricato un worklet con il nome "passthrough", prima di dichiarare l'animazione:
// 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 che abbiamo usato 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 ti 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.
Dimmi il tuo stato locale.
Come accennato in precedenza, uno dei problemi principali del worklet dell'animazione sono le animazioni stateful. 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à su due
in base alla quale direzione 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
offre nuove possibilità e ti consente di creare 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
.
Potresti aver indovinato, ScrollTimeline
non usa 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 di 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 currentTime
della sequenza temporale sarà NaN
. Quindi, soprattutto con il design adattabile in mente, devi sempre essere preparato per NaN
come currentTime
. In genere, è opportuno 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). Il worklet dell'animazione consente di implementare questi effetti in modo semplice e ad alte prestazioni. 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, il cui compito è (e stiamo semplifico molto in questo caso) per disporre strati e texture e quindi utilizzare la GPU per aggiornare lo schermo il più regolarmente possibile, preferibilmente alla velocità massima possibile per l'aggiornamento dello schermo (in genere a 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. 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 può "slittare", in base alla specifica. Rallenta rispetto al compositore, che può riutilizzare i dati dell'ultimo frame per mantenere alto il frame rate. Visivamente, questo potrebbe sembrare un problema di aggiornamento, 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 creare animazioni che offrono un nuovo livello di fedeltà visiva sul web. Tuttavia, la progettazione delle API consente anche di rendere la tua app più resiliente ai jank, ottenendo contemporaneamente l'accesso a tutte le novità.
Il worklet di animazione è nella versione Canary e puntiamo a una prova dell'origine con Chrome 71. Siamo ansiosi di ricevere le vostre nuove fantastiche esperienze sul web e di sapere cosa possiamo migliorare. Esiste anche un polyfill che fornisce la stessa API, ma non fornisce l'isolamento delle prestazioni.
Tieni presente che le transizioni CSS e le animazioni CSS sono comunque 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.