Boostez les animations de votre application Web
En bref : Animation Worklet vous permet d'écrire des animations impératives qui s'exécutent à la fréquence d'images native de l'appareil pour une fluidité sans saccades™, de rendre vos animations plus résistantes aux saccades du thread principal et de les associer au défilement au lieu du temps. Animation Worklet est disponible dans Chrome Canary (derrière le commutateur "Experimental Web Platform features"). Nous prévoyons une évaluation Origin Trial pour Chrome 71. Vous pouvez commencer à l'utiliser comme amélioration progressive dès aujourd'hui.
Une autre API Animation ?
En fait, non. Il s'agit d'une extension de ce que nous avons déjà, et pour cause ! Commençons par le commencement. Si vous souhaitez animer un élément DOM sur le Web aujourd'hui, vous avez deux choix et demi : les transitions CSS pour les transitions simples de A à B, les animations CSS pour les animations potentiellement cycliques et plus complexes basées sur le temps, et l'API Web Animations (WAAPI) pour les animations presque arbitrairement complexes. La matrice de compatibilité de WAAPI est plutôt sombre, mais elle est en train de s'améliorer. En attendant, il existe un polyfill.
Ces méthodes ont toutes en commun d'être sans état et axées sur le temps. Toutefois, certains effets que les développeurs tentent d'obtenir ne sont ni pilotés par le temps, ni sans état. Par exemple, le célèbre défilement parallaxe est, comme son nom l'indique, piloté par le défilement. Il est étonnamment difficile d'implémenter un scroller parallax performant sur le Web aujourd'hui.
Qu'en est-il de l'architecture sans état ? Pensez par exemple à la barre d'adresse de Chrome sur Android. Si vous faites défiler la page vers le bas, il disparaît de l'écran. Mais dès que vous faites défiler l'écran vers le haut, il réapparaît, même si vous êtes à mi-chemin de la page. L'animation dépend non seulement de la position de défilement, mais aussi de la direction de défilement précédente. Il est avec état.
Un autre problème concerne le style des barres de défilement. Ils sont notoirement difficiles à styliser, ou du moins pas assez. Et si je veux un nyan cat comme barre de défilement ? Quelle que soit la technique choisie, la création d'une barre de défilement personnalisée n'est ni performante ni facile.
Le problème, c'est que toutes ces choses sont difficiles, voire impossibles, à implémenter efficacement. La plupart d'entre eux s'appuient sur des événements et/ou requestAnimationFrame
, ce qui peut vous maintenir à 60 fps, même si votre écran est capable de fonctionner à 90 fps, 120 fps ou plus, et utiliser une fraction de votre précieux budget de frames de thread principal.
Animation Worklet étend les capacités de la pile d'animations du Web pour faciliter ce type d'effets. Avant de commencer, assurons-nous de maîtriser les bases des animations.
Présentation des animations et des timelines
WAAPI et Animation Worklet utilisent largement les timelines pour vous permettre d'orchestrer les animations et les effets comme vous le souhaitez. Cette section est un rappel ou une introduction rapide aux timelines et à leur fonctionnement avec les animations.
Chaque document possède document.timeline
. Il commence à 0 lorsque le document est créé et compte les millisecondes depuis la création du document. Toutes les animations d'un document fonctionnent par rapport à cette timeline.
Pour rendre les choses un peu plus concrètes, examinons cet extrait de code 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();
Lorsque nous appelons animation.play()
, l'animation utilise le currentTime
de la timeline comme heure de début. Notre animation a un délai de 3 000 ms, ce qui signifie qu'elle commencera (ou deviendra "active") lorsque la timeline atteindra `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`. L'idée est que la timeline contrôle où nous en sommes dans notre animation.
Une fois que l'animation a atteint la dernière image clé, elle revient à la première image clé et lance la prochaine itération de l'animation. Ce processus se répète trois fois au total, car nous avons défini iterations: 3
. Si nous voulions que l'animation ne s'arrête jamais, nous écririons iterations: Number.POSITIVE_INFINITY
. Voici le résultat du code ci-dessus.
L'API WAAPI est incroyablement puissante et propose de nombreuses autres fonctionnalités, comme l'interpolation, les décalages de début, les pondérations des images clés et le comportement de remplissage, qui dépasseraient le champ d'application de cet article. Pour en savoir plus, je vous recommande de lire cet article sur les animations CSS sur CSS Tricks.
Écrire un Animation Worklet
Maintenant que nous avons compris le concept de chronologie, nous pouvons commencer à examiner les worklets d'animation et comment ils vous permettent de modifier les chronologies. L'API Animation Worklet n'est pas seulement basée sur WAAPI, mais constitue également une primitive de niveau inférieur qui explique le fonctionnement de WAAPI, dans le sens du Web extensible. En termes de syntaxe, elles sont incroyablement similaires :
Worklet d'animation | 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 différence réside dans le premier paramètre, qui est le nom du worklet qui pilote cette animation.
Détection des fonctionnalités
Chrome est le premier navigateur à proposer cette fonctionnalité. Vous devez donc vous assurer que votre code ne s'attend pas simplement à ce que AnimationWorklet
soit là. Avant de charger le worklet, nous devons donc détecter si le navigateur de l'utilisateur est compatible avec AnimationWorklet
à l'aide d'une simple vérification :
if ('animationWorklet' in CSS) {
// AnimationWorklet is supported!
}
Charger un worklet
Les worklets sont un nouveau concept introduit par le groupe de travail Houdini pour faciliter la création et l'évolutivité de nombreuses nouvelles API. Nous aborderons les détails des worklets un peu plus tard, mais pour simplifier, vous pouvez les considérer comme des threads bon marché et légers (comme les workers) pour le moment.
Nous devons nous assurer d'avoir chargé un worklet portant le nom "passthrough" avant de déclarer l'animation :
// 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;
}
}
);
Que se passe-t-il ici ? Nous enregistrons une classe en tant qu'animateur à l'aide de l'appel registerAnimator()
d'AnimationWorklet, en lui donnant le nom "passthrough".
Il s'agit du même nom que celui utilisé dans le constructeur WorkletAnimation()
ci-dessus. Une fois l'enregistrement terminé, la promesse renvoyée par addModule()
sera résolue et nous pourrons commencer à créer des animations à l'aide de ce worklet.
La méthode animate()
de notre instance sera appelée pour chaque frame que le navigateur souhaite afficher, en transmettant le currentTime
de la timeline de l'animation ainsi que l'effet en cours de traitement. Nous n'avons qu'un seul effet, KeyframeEffect
, et nous utilisons currentTime
pour définir le localTime
de l'effet. C'est pourquoi cet Animator est appelé "passthrough" (transparence). Avec ce code pour le worklet, la WAAPI et AnimationWorklet ci-dessus se comportent exactement de la même manière, comme vous pouvez le voir dans la démonstration.
Temps
Le paramètre currentTime
de notre méthode animate()
est le currentTime
de la timeline que nous avons transmise au constructeur WorkletAnimation()
. Dans l'exemple précédent, nous avons simplement transmis cette heure à l'effet. Mais comme il s'agit de code JavaScript, nous pouvons distordre le temps 💫
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)
);
}
}
);
Nous prenons le Math.sin()
de currentTime
et remappons cette valeur à la plage [0 ; 2000], qui est la plage de temps pour laquelle notre effet est défini. Maintenant, l'animation est très différente, sans que nous ayons modifié les images clés ni les options de l'animation. Le code du worklet peut être arbitrairement complexe et vous permet de définir par programmation les effets qui sont joués, dans quel ordre et dans quelle mesure.
Options sur options
Vous pouvez réutiliser un worklet et modifier ses nombres. Pour cette raison, le constructeur WorkletAnimation vous permet de transmettre un objet d'options au 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();
Dans cet exemple, les deux animations sont pilotées par le même code, mais avec des options différentes.
Donne-moi ton état local !
Comme je l'ai mentionné précédemment, l'un des principaux problèmes que le worklet d'animation vise à résoudre concerne les animations avec état. Les worklets d'animation sont autorisés à conserver un état. Toutefois, l'une des principales caractéristiques des worklets est qu'ils peuvent être migrés vers un autre thread ou même être détruits pour économiser des ressources, ce qui détruirait également leur état. Pour éviter la perte d'état, le worklet d'animation propose un crochet appelé avant la destruction d'un worklet, que vous pouvez utiliser pour renvoyer un objet d'état. Cet objet sera transmis au constructeur lorsque le worklet sera recréé. Lors de la création initiale, ce paramètre sera défini sur 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,
};
}
}
);
Chaque fois que vous actualisez cette démo, vous avez une chance sur deux de voir le carré tourner dans un sens ou dans l'autre. Si le navigateur devait détruire le worklet et le migrer vers un autre thread, un autre appel Math.random()
serait effectué lors de la création, ce qui pourrait entraîner un changement de direction soudain. Pour éviter cela, nous renvoyons la direction choisie au hasard pour les animations en tant que state et l'utilisons dans le constructeur, si elle est fournie.
S'accrocher au continuum espace-temps : ScrollTimeline
Comme l'a montré la section précédente, AnimationWorklet nous permet de définir par programmation comment l'avancement de la timeline affecte les effets de l'animation. Mais jusqu'à présent, notre chronologie a toujours été document.timeline
, qui suit le temps.
ScrollTimeline
ouvre de nouvelles possibilités et vous permet de contrôler les animations avec le défilement au lieu du temps. Nous allons réutiliser notre tout premier worklet "passthrough" pour cette démonstration :
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();
Au lieu de transmettre document.timeline
, nous créons un ScrollTimeline
.
Vous l'avez peut-être deviné, ScrollTimeline
n'utilise pas le temps, mais la position de défilement de scrollSource
pour définir currentTime
dans le worklet. Si l'utilisateur a fait défiler l'écran jusqu'en haut (ou à gauche), currentTime = 0
est défini sur timeRange
. S'il a fait défiler l'écran jusqu'en bas (ou à droite), currentTime
est défini sur timeRange
. Si vous faites défiler la zone dans cette démonstration, vous pouvez contrôler la position de la zone rouge.
Si vous créez un ScrollTimeline
avec un élément qui ne défile pas, le currentTime
de la timeline sera NaN
. En particulier avec la conception responsive à l'esprit, vous devez toujours être prêt pour NaN
en tant que currentTime
. Il est souvent judicieux de définir la valeur par défaut sur 0.
L'association d'animations à la position de défilement est une fonctionnalité très attendue, mais qui n'a jamais été réellement atteinte à ce niveau de fidélité (à l'exception de solutions de contournement peu pratiques avec CSS3D). Les worklets d'animation permettent d'implémenter ces effets de manière simple tout en étant très performants. Par exemple, un effet de défilement parallax comme celui de cette démonstration montre qu'il ne faut plus que quelques lignes pour définir une animation pilotée par le défilement.
dans le détail
Worklets
Les worklets sont des contextes JavaScript avec une portée isolée et une surface d'API très petite. La petite surface de l'API permet une optimisation plus agressive du navigateur, en particulier sur les appareils d'entrée de gamme. De plus, les worklets ne sont pas liés à une boucle d'événement spécifique, mais peuvent être déplacés entre les threads si nécessaire. Cela est particulièrement important pour AnimationWorklet.
NSync du compositeur
Vous savez peut-être que certaines propriétés CSS sont rapides à animer, tandis que d'autres ne le sont pas. Certaines propriétés nécessitent simplement un peu de travail sur le GPU pour être animées, tandis que d'autres forcent le navigateur à réorganiser l'ensemble du document.
Dans Chrome (comme dans de nombreux autres navigateurs), nous avons un processus appelé "compositor", dont le rôle est (et je simplifie beaucoup ici) d'organiser les calques et les textures, puis d'utiliser le GPU pour actualiser l'écran aussi régulièrement que possible, idéalement aussi vite que l'écran peut s'actualiser (généralement 60 Hz). Selon les propriétés CSS animées, le navigateur peut simplement avoir besoin que le compositeur fasse son travail, tandis que d'autres propriétés doivent exécuter la mise en page, une opération que seul le thread principal peut effectuer. Selon les propriétés que vous prévoyez d'animer, votre worklet d'animation sera lié au thread principal ou s'exécutera dans un thread distinct en synchronisation avec le compositeur.
Une tape sur les doigts
Il n'y a généralement qu'un seul processus de compositeur, qui peut être partagé entre plusieurs onglets, car le GPU est une ressource très sollicitée. Si le compositeur est bloqué d'une manière ou d'une autre, l'ensemble du navigateur s'arrête et ne répond plus aux saisies de l'utilisateur. Cela doit être évité à tout prix. Que se passe-t-il si votre worklet ne peut pas fournir les données dont le compositeur a besoin à temps pour que le frame soit rendu ?
Dans ce cas, le worklet est autorisé à "glisser", conformément aux spécifications. Il est en retard sur le compositeur, qui est autorisé à réutiliser les données du dernier frame pour maintenir la fréquence d'images. Visuellement, cela ressemblera à du jank, mais la grande différence est que le navigateur reste réactif aux saisies de l'utilisateur.
Conclusion
AnimationWorklet présente de nombreuses facettes et offre de nombreux avantages pour le Web. Les avantages évidents sont un meilleur contrôle des animations et de nouvelles façons de les piloter pour apporter un nouveau niveau de fidélité visuelle au Web. Toutefois, la conception des API vous permet également de rendre votre application plus résistante aux saccades tout en accédant à toutes les nouvelles fonctionnalités.
Animation Worklet est disponible dans Canary et nous prévoyons un Origin Trial avec Chrome 71. Nous avons hâte de découvrir vos nouvelles expériences Web et de savoir ce que nous pouvons améliorer. Il existe également un polyfill qui vous offre la même API, mais pas l'isolation des performances.
N'oubliez pas que les transitions et animations CSS restent des options valables et peuvent être beaucoup plus simples pour les animations de base. Mais si vous avez besoin d'un peu de fantaisie, AnimationWorklet est là pour vous !