Worklet d'animation de Houdini

Optimiser les animations de votre application Web

Résumé:Le worklet d'animation vous permet d'écrire des animations impératives qui s'exécutent à la fréquence d'images native de l'appareil pour une fluidité optimale™, de rendre vos animations plus résistantes aux à-coups du thread principal et de les associer au défilement plutôt qu'au temps. Le worklet d'animation est disponible dans Chrome Canary (derrière l'option "Experimental Web Platform features") et nous prévoyons une phase d'évaluation pour Chrome 71. Vous pouvez commencer à l'utiliser en tant qu'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 de bonnes raisons ! Commençons par le commencement. Si vous souhaitez animer un élément DOM sur le Web aujourd'hui, vous avez le choix entre deux options et demie: les transitions CSS pour les transitions simples de A à B, les animations CSS pour les animations temporelles potentiellement cycliques et plus complexes, et l'API Web Animations (WAAPI) pour les animations presque arbitrairement complexes. La matrice de compatibilité de WAAPI semble assez sombre, mais elle est en hausse. En attendant, un polyfill est disponible.

Toutes ces méthodes ont un point en commun : elles sont sans état et basées sur le temps. Toutefois, certains des effets que les développeurs essaient ne sont ni basés sur le temps, ni sans état. Par exemple, le tristement célèbre défilement parallaxe est, comme son nom l'indique, basé sur le défilement. Il est étonnamment difficile d'implémenter un défilement parallaxe performant sur le Web aujourd'hui.

Qu'en est-il de l'état ? Pensez, par exemple, à la barre d'adresse de Chrome sur Android. Si vous faites défiler la page vers le bas, elle disparaît. Mais dès que vous faites défiler la page vers le haut, elle 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 associé à un état.

Autre problème : le style des barres de défilement. Ils sont réputés pour être difficiles à styliser, ou du moins pas assez. Que faire si je souhaite utiliser un nyan cat comme barre de défilement ? Quelle que soit la technique que vous choisissez, créer une barre de défilement personnalisée n'est ni performant, ni facile.

L'idée est que toutes ces choses sont maladroites et difficiles, voire impossibles, à implémenter efficacement. La plupart d'entre eux reposent sur des événements et/ou sur requestAnimationFrame, ce qui peut vous maintenir à 60 FPS, même lorsque votre écran peut fonctionner à 90 FPS, 120 FPS ou plus, et utiliser une fraction de votre précieux budget de frames de thread principal.

Le worklet d'animation étend les fonctionnalités de la pile d'animations du Web pour faciliter ce type d'effets. Avant de nous lancer, assurons-nous de connaître les bases des animations.

Présentation des animations et des timelines

WAAPI et Animation Worklet utilisent largement les chronologies pour vous permettre d'orchestrer les animations et les effets comme vous le souhaitez. Cette section est un rappel rapide ou une introduction aux chronologies et à leur fonctionnement avec les animations.

Chaque document possède un document.timeline. Il commence à 0 lorsque le document est créé et compte les millisecondes depuis son existence. Toutes les animations d'un document fonctionnent par rapport à cette chronologie.

Pour que tout soit un peu plus concret, 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 thedurationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline'scurrentTimeisstartTime + 3000 + 1000and the last keyframe atstartTime + 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 l'itération suivante de l'animation. Ce processus se répète au total trois fois depuis que nous avons défini iterations: 3. Si nous souhaitons que l'animation ne s'arrête jamais, nous devons écrire iterations: Number.POSITIVE_INFINITY. Voici le résultat du code ci-dessus.

WAAPI est incroyablement puissante. Elle propose de nombreuses autres fonctionnalités, comme l'atténuation, les décalages de début, les pondérations des clés-images et le comportement de remplissage, qui dépassent le cadre de cet article. Pour en savoir plus, je vous recommande de lire cet article sur les animations CSS sur CSS Tricks.

Écrire un worklet d'animation

Maintenant que nous avons compris le concept de chronologie, nous pouvons commencer à examiner le worklet d'animation et comment il vous permet de manipuler les chronologies. L'API Animation Worklet n'est pas seulement basée sur WAAPI, mais est, dans le sens du Web extensible, une primitive de bas niveau qui explique le fonctionnement de WAAPI. 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 se trouve dans le premier paramètre, qui est le nom du worklet qui gère cette animation.

Détection de 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 présent. Par conséquent, avant de charger le worklet, nous devons 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 la task force Houdini pour faciliter la création et l'évolutivité de nombreuses nouvelles API. Nous verrons plus en détail les worklets un peu plus tard, mais pour simplifier, vous pouvez les considérer comme des threads peu coûteux et légers (comme des workers) pour le moment.

Nous devons nous assurer d'avoir chargé un worklet nommé "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() de l'animationWorklet, en lui donnant le nom "passthrough". Il s'agit du même nom que celui que nous avons 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 actuellement 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 animateur est appelé "passthrough". Avec ce code pour le worklet, la WAAPI et l'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 chronologie que nous avons transmise au constructeur WorkletAnimation(). Dans l'exemple précédent, nous avons simplement transmis ce temps à l'effet. Mais comme il s'agit de code JavaScript, nous pouvons déformer 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 sur la plage [0; 2000], qui correspond à la plage de temps pour laquelle notre effet est défini. L'animation est maintenant très différente, sans avoir 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 de manière programmatique les effets à lire, dans quel ordre et dans quelle mesure.

Options sur les options

Vous pouvez réutiliser un worklet et modifier ses numéros. C'est pourquoi 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 gérées par le même code, mais avec des options différentes.

Donne-moi ton état local !

Comme je l'ai indiqué précédemment, l'un des principaux problèmes que le worklet d'animation vise à résoudre est les animations avec état. Les worklets d'animation sont autorisés à conserver l'état. Toutefois, l'une des principales caractéristiques des worklets est qu'ils peuvent être migrés vers un autre thread ou même détruits pour économiser des ressources, ce qui détruit é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 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émonstration, vous avez une chance sur deux que le carré tourne dans une direction donnée. Si le navigateur devait désassembler 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 soudain de direction. Pour éviter cela, nous renvoyons la direction choisie au hasard pour les animations en tant qu'état et nous l'utilisons dans le constructeur, le cas échéant.

S'intégrer au continuum espace-temps: ScrollTimeline

Comme indiqué dans la section précédente, AnimationWorklet nous permet de définir de manière programmatique l'impact de l'avancement de la timeline sur 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 piloter les animations avec le défilement au lieu du temps. Nous allons réutiliser notre tout premier worklet de "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. Comme 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 le défilement est effectué jusqu'en haut (ou à gauche), la valeur currentTime = 0 est renvoyée. Si le défilement est effectué jusqu'en bas (ou à droite), la valeur timeRange est renvoyée.currentTime Si vous faites défiler la zone de cette démo, 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 chronologie sera NaN. Par conséquent, en particulier avec le responsive design à l'esprit, vous devez toujours être prêt à utiliser NaN comme currentTime. Il est souvent judicieux de définir une valeur par défaut de 0.

L'association d'animations à la position de défilement est une fonctionnalité recherchée depuis longtemps, mais qui n'a jamais été vraiment obtenue à ce niveau de fidélité (à l'exception de solutions de contournement avec CSS3D). Le worklet d'animation permet d'implémenter ces effets de manière simple, tout en étant très performant. Par exemple, un effet de défilement en parallaxe comme dans cette démo montre qu'il ne faut plus que quelques lignes pour définir une animation basée sur le défilement.

dans le détail

Worklets

Les worklets sont des contextes JavaScript avec un champ d'application isolé et une très petite surface d'API. 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énements spécifique, mais peuvent être déplacés entre les threads si nécessaire. Cela est particulièrement important pour AnimationWorklet.

Compositor NSync

Vous savez peut-être que certaines propriétés CSS sont animées rapidement, tandis que d'autres ne le sont pas. Certaines propriétés n'ont besoin que d'un peu de travail sur le GPU pour être animées, tandis que d'autres obligent le navigateur à réorganiser l'intégralité du document.

Dans Chrome (comme dans de nombreux autres navigateurs), il existe un processus appelé "compilateur", dont le rôle est (et je simplifie beaucoup ici) d'organiser les calques et les textures, puis d'utiliser le GPU pour mettre à jour l'écran aussi régulièrement que possible, dans l'idéal aussi rapidement que l'écran peut être mis à jour (généralement 60 Hz). Selon les propriétés CSS animées, le navigateur peut simplement demander au moteur de rendu de faire 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 exécuté dans un thread distinct synchronisé avec le moteur de rendu.

Coup de poignet

Il n'y a généralement qu'un seul processus de compositeur qui est potentiellement partagé entre plusieurs onglets, car le GPU est une ressource très convoité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 entrées 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 affiché ?

Dans ce cas, le worklet est autorisé, conformément aux spécifications, à "glisser". Il est en retard sur le compositeur, et le compositeur est autorisé à réutiliser les données du dernier frame pour maintenir la fréquence d'images. Visuellement, cela ressemble à un à-coup, mais la grande différence est que le navigateur reste réactif à l'entrée utilisateur.

Conclusion

AnimationWorklet présente de nombreux aspects et offre de nombreux avantages sur le Web. Les avantages évidents sont un meilleur contrôle des animations et de nouvelles façons de les animer pour apporter un nouveau niveau de fidélité visuelle au Web. Mais la conception des API vous permet également de rendre votre application plus résistante aux à-coups tout en ayant accès à toutes les nouvelles fonctionnalités.

Le worklet d'animation est disponible dans Canary. Nous prévoyons un test en phase d'origine avec Chrome 71. Nous attendons avec impatience vos nouvelles expériences Web et vos commentaires sur ce que nous pouvons améliorer. Il existe également un polyfill qui fournit la même API, mais n'offre pas l'isolation des performances.

N'oubliez pas que les transitions CSS et les animations CSS restent des options valides et peuvent être beaucoup plus simples pour les animations de base. Toutefois, si vous avez besoin de plus de sophistication, AnimationWorklet est là pour vous aider.