Animations-Worklet von Houdini

Animationen Ihrer Webanwendung optimieren

Zusammenfassung:Mit Animation Worklets können Sie imperative Animationen schreiben, die mit der nativen Framerate des Geräts ausgeführt werden, um eine besonders flüssige Darstellung ohne Ruckler zu ermöglichen. Außerdem sind sie beständiger gegen Ruckler im Hauptthread und können an das Scrollen statt an die Zeit gekoppelt werden. Animation Worklet ist in Chrome Canary verfügbar (hinter dem Flag „Experimental Web Platform features“). Wir planen einen Ursprungstest für Chrome 71. Sie können es schon heute als progressive Verbesserung verwenden.

Eine weitere Animation API?

Nein, es ist eine Erweiterung dessen, was wir bereits haben, und das aus gutem Grund. Fangen wir am Anfang an. Wenn Sie ein DOM-Element im Web animieren möchten, haben Sie derzeit zweieinhalb Möglichkeiten: CSS-Übergänge für einfache Übergänge von A nach B, CSS-Animationen für potenziell zyklische, komplexere zeitbasierte Animationen und die Web Animations API (WAAPI) für nahezu beliebig komplexe Animationen. Die Supportmatrix der WAAPI sieht ziemlich düster aus, aber es wird besser. Bis dahin gibt es einen Polyfill.

Allen diesen Methoden ist gemeinsam, dass sie zustandslos und zeitgesteuert sind. Einige der Effekte, die Entwickler ausprobieren, sind jedoch weder zeitgesteuert noch zustandslos. Der berüchtigte Parallaxen-Scroller ist beispielsweise, wie der Name schon sagt, scrollgesteuert. Die Implementierung eines leistungsstarken Paralax-Scrollers im Web ist heute überraschend schwierig.

Und was ist mit der Zustandslosigkeit? Denken Sie zum Beispiel an die Adressleiste von Chrome auf Android-Geräten. Wenn Sie nach unten scrollen, wird es ausgeblendet. Aber sobald Sie nach oben scrollen, kehrt es zurück, auch wenn Sie sich auf halber Höhe der Seite befinden. Die Animation hängt nicht nur von der Scrollposition, sondern auch von der vorherigen Scrollrichtung ab. Es ist zustandsorientiert.

Ein weiteres Problem ist das Design von Bildlaufleisten. Sie sind notorisch schwer zu stylen – oder zumindest nicht stilvoll genug. Was ist, wenn ich eine Nyan-Katze als Bildlaufleiste haben möchte? Unabhängig von der gewählten Methode ist das Erstellen einer benutzerdefinierten Bildlaufleiste weder leistungsstark noch einfach.

Der Punkt ist, dass all diese Dinge umständlich sind und sich nur schwer bis gar nicht effizient umsetzen lassen. Die meisten davon basieren auf Ereignissen und/oder requestAnimationFrame. Dadurch wird möglicherweise eine Bildrate von 60 fps beibehalten, auch wenn Ihr Display mit 90 fps, 120 fps oder mehr laufen könnte und nur ein Bruchteil Ihres wertvollen Frame-Budgets für den Hauptthread verwendet wird.

Das Animation Worklet erweitert die Funktionen des Animationsstapels im Web, um diese Art von Effekten zu vereinfachen. Bevor wir loslegen, sollten wir uns noch einmal die Grundlagen von Animationen ins Gedächtnis rufen.

Einführung in Animationen und Zeitachsen

WAAPI und Animation Worklet nutzen Zeitachsen, damit Sie Animationen und Effekte nach Belieben orchestrieren können. In diesem Abschnitt erhalten Sie eine kurze Auffrischung oder Einführung in Zeitleisten und ihre Funktionsweise in Verbindung mit Animationen.

Jedes Dokument hat eine document.timeline. Der Wert beginnt bei 0, wenn das Dokument erstellt wird, und zählt die Millisekunden seit dem Erstellen des Dokuments. Alle Animationen eines Dokuments funktionieren relativ zu dieser Zeitachse.

Sehen wir uns zum besseren Verständnis dieses WAAPI-Snippet an:

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();

Wenn wir animation.play() aufrufen, verwendet die Animation currentTime der Zeitleiste als Startzeit. Unsere Animation hat eine Verzögerung von 3.000 Millisekunden. Das bedeutet, dass die Animation beginnt (oder „aktiv“ wird), wenn die Zeitachse den Wert „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. Der Zeitplan steuert, wo wir uns in der Animation befinden.

Sobald die Animation den letzten Keyframe erreicht hat, springt sie zurück zum ersten Keyframe und startet die nächste Iteration der Animation. Da wir iterations: 3 festgelegt haben, wird dieser Vorgang insgesamt dreimal wiederholt. Wenn die Animation nie enden soll, geben wir iterations: Number.POSITIVE_INFINITY ein. Hier ist das Ergebnis des Codes oben.

WAAPI ist unglaublich leistungsstark und bietet viele weitere Funktionen wie Ease-In/Ease-Out, Start-Offset, Keyframe-Bewertungen und Füllverhalten, die den Rahmen dieses Artikels sprengen würden. Weitere Informationen finden Sie in diesem Artikel zu CSS-Animationen auf CSS Tricks.

Animation Worklet schreiben

Nachdem wir das Konzept von Zeitleisten kennen, können wir uns das Animation Worklet und die Möglichkeiten ansehen, die es bietet. Die Animation Worklet API basiert nicht nur auf WAAPI, sondern ist im Sinne des extensible web ein Primitives auf niedrigerer Ebene, das erklärt, wie WAAPI funktioniert. Sie sind sich in der Syntax sehr ähnlich:

Animations-Worklet 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();
        

Der Unterschied besteht im ersten Parameter, dem Namen des Worklets, das diese Animation steuert.

Funktionserkennung

Chrome ist der erste Browser, in dem diese Funktion verfügbar ist. Achten Sie also darauf, dass in Ihrem Code nicht nur AnimationWorklet erwartet wird. Bevor wir das Worklet laden, sollten wir mit einer einfachen Prüfung feststellen, ob der Browser des Nutzers AnimationWorklet unterstützt:

if ('animationWorklet' in CSS) {
  // AnimationWorklet is supported!
}

Worklet laden

Worklets sind ein neues Konzept, das von der Houdini-Taskforce eingeführt wurde, um viele der neuen APIs einfacher zu erstellen und zu skalieren. Wir werden später noch genauer auf Worklets eingehen. Für den Moment können Sie sie sich aber als kostengünstige und schlanke Threads (wie Worker) vorstellen.

Wir müssen ein Worklet mit dem Namen „passthrough“ geladen haben, bevor wir die Animation deklarieren:

// 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;
    }
  }
);

Was ist hier los? Wir registrieren eine Klasse als Animator mit dem registerAnimator()-Aufruf des AnimationWorklets und geben ihr den Namen „passthrough“. Das ist derselbe Name, den wir oben im WorkletAnimation()-Konstruktor verwendet haben. Sobald die Registrierung abgeschlossen ist, wird das von addModule() zurückgegebene Versprechen erfüllt und wir können mithilfe dieses Worklets Animationen erstellen.

Die animate()-Methode unserer Instanz wird für jeden Frame aufgerufen, den der Browser rendern möchte. Dabei werden der currentTime der Zeitleiste der Animation sowie der Effekt übergeben, der gerade verarbeitet wird. Wir haben nur einen Effekt, den KeyframeEffect, und verwenden currentTime, um die localTime des Effekts festzulegen. Daher wird dieser Animator als „Passthrough“ bezeichnet. Mit diesem Code für das Worklet verhalten sich die WAAPI und das AnimationWorklet oben genau gleich, wie in der Demo zu sehen ist.

Zeit

Der Parameter currentTime unserer Methode animate() ist der currentTime der Zeitachse, den wir an den Konstruktor von WorkletAnimation() übergeben haben. Im vorherigen Beispiel haben wir diese Zeit einfach an den Effekt übergeben. Da es sich aber um JavaScript-Code handelt, können wir die Zeit verdrehen 💫

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)
      );
    }
  }
);

Wir nehmen den Math.sin() des currentTime und ordnen diesen Wert dem Bereich [0; 2000] zu, dem Zeitraum, für den unser Effekt definiert ist. Jetzt sieht die Animation ganz anders aus, ohne dass ich die Keyframes oder die Optionen der Animation geändert habe. Der Worklet-Code kann beliebig komplex sein und ermöglicht es, programmatisch zu definieren, welche Effekte in welcher Reihenfolge und in welchem Umfang wiedergegeben werden.

Optionen über Optionen

Vielleicht möchten Sie ein Worklet wiederverwenden und die Zahlen ändern. Aus diesem Grund können Sie dem Worklet über den Konstruktor von WorkletAnimation ein Optionsobjekt übergeben:

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 diesem Beispiel werden beide Animationen mit demselben Code, aber mit unterschiedlichen Optionen ausgeführt.

Gib mir deinen Bundesland!

Wie bereits erwähnt, soll mit dem Animation Worklet vor allem ein Problem gelöst werden: zustandsabhängige Animationen. Animations-Worklets dürfen den Status speichern. Eine der Hauptfunktionen von Worklets besteht jedoch darin, dass sie zu einem anderen Thread migriert oder sogar zerstört werden können, um Ressourcen zu sparen, wodurch auch ihr Status gelöscht wird. Um den Verlust des Zustands zu verhindern, bietet das Animation-Worklet einen Hook, der bevor ein Worklet zerstört wird, aufgerufen wird. Mit diesem Hook können Sie ein Zustandsobjekt zurückgeben. Dieses Objekt wird an den Konstruktor übergeben, wenn das Worklet neu erstellt wird. Bei der Ersterstellung hat dieser Parameter den Wert 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,
      };
    }
  }
);

Jedes Mal, wenn Sie diese Demo aktualisieren, haben Sie eine 50:50-Chance, in welche Richtung sich das Quadrat dreht. Wenn der Browser das Worklet auflöst und in einen anderen Thread migriert, wird bei der Erstellung ein weiterer Math.random()-Aufruf ausgeführt, was zu einer plötzlichen Richtungsänderung führen kann. Um dies zu verhindern, geben wir die zufällig ausgewählte Richtung der Animation als state zurück und verwenden sie im Konstruktor, sofern vorhanden.

An das Raum-Zeit-Kontinuum anknüpfen: ScrollTimeline

Wie im vorherigen Abschnitt gezeigt, können wir mit AnimationWorklet programmatisch definieren, wie sich das Vor- und Zurückspulen der Zeitleiste auf die Effekte der Animation auswirkt. Bisher war unsere Zeitachse immer document.timeline, die die Zeit erfasst.

ScrollTimeline eröffnet neue Möglichkeiten und ermöglicht es, Animationen durch Scrollen statt durch Zeit zu steuern. Wir verwenden für diese Demo unser allererstes „Passthrough“-Worklet:

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();

Anstatt document.timeline weiterzugeben, erstellen wir eine neue ScrollTimeline. Wie Sie vielleicht schon vermutet haben, wird in ScrollTimeline nicht die Zeit, sondern die Scrollposition von scrollSource verwendet, um die currentTime im Worklet festzulegen. Wenn Sie ganz nach oben (oder links) gescrollt haben, wird currentTime = 0 angezeigt. Wenn Sie ganz nach unten (oder rechts) gescrollt haben, wird currentTime auf timeRange gesetzt. Wenn Sie in dieser Demo das Feld scrollen, können Sie die Position des roten Felds steuern.

Wenn Sie eine ScrollTimeline mit einem Element erstellen, das nicht scrollt, ist NaN die currentTime der Zeitachse. Insbesondere im Hinblick auf responsives Design sollten Sie immer auf NaN als currentTime vorbereitet sein. Häufig ist es sinnvoll, den Standardwert auf „0“ festzulegen.

Die Verknüpfung von Animationen mit der Scrollposition wurde schon lange angestrebt, aber nie wirklich in dieser Qualität erreicht (abgesehen von hakeligen Workarounds mit CSS3D). Mit dem Animation Worklet können diese Effekte auf einfache Weise und mit hoher Leistung implementiert werden. Beispiel: Mit einem Paralax-Scrolling-Effekt wie in dieser Demo lässt sich eine scrollbasierte Animation jetzt mit nur wenigen Zeilen definieren.

Funktionsweise

Worklets

Worklets sind JavaScript-Kontexte mit einem isolierten Umfang und einer sehr kleinen API-Oberfläche. Die kleine API-Oberfläche ermöglicht eine aggressivere Optimierung durch den Browser, insbesondere auf Low-End-Geräten. Außerdem sind Worklets nicht an eine bestimmte Ereignisschleife gebunden, sondern können bei Bedarf zwischen Threads verschoben werden. Das ist besonders für AnimationWorklet wichtig.

Compositor NSync

Bestimmte CSS-Eigenschaften lassen sich schnell animieren, andere dagegen nicht. Für einige Eigenschaften ist nur eine gewisse GPU-Leistung erforderlich, um sie zu animieren, während andere den Browser dazu zwingen, das gesamte Dokument neu zu layouten.

In Chrome (wie in vielen anderen Browsern) gibt es einen Prozess namens „Compositor“. Dieser ist dafür verantwortlich, Ebenen und Texturen anzuordnen und dann die GPU zu verwenden, um den Bildschirm so regelmäßig wie möglich zu aktualisieren, idealerweise so schnell wie der Bildschirm aktualisiert werden kann (normalerweise 60 Hz). Je nachdem, welche CSS-Eigenschaften animiert werden, muss der Browser möglicherweise nur den Renderer verwenden, während für andere Eigenschaften das Layout ausgeführt werden muss. Dies ist eine Operation, die nur der Hauptthread ausführen kann. Je nachdem, welche Eigenschaften Sie animieren möchten, wird Ihr Animations-Worklet entweder an den Hauptthread gebunden oder in einem separaten Thread ausgeführt, der mit dem Renderer synchronisiert ist.

Am Handgelenk

Normalerweise gibt es nur einen einzigen Compositor-Prozess, der potenziell für mehrere Tabs gemeinsam genutzt wird, da die GPU eine stark umkämpfte Ressource ist. Wenn der Compositor blockiert wird, kommt der gesamte Browser zum Stillstand und reagiert nicht mehr auf Nutzereingaben. Das muss unbedingt vermieden werden. Was passiert, wenn Ihr Worklet die Daten, die der Compositor benötigt, nicht rechtzeitig für das Rendern des Frames bereitstellen kann?

In diesem Fall darf das Worklet gemäß den Spezifikationen „verrutschen“. Es fällt hinter den Compositor zurück und der Compositor darf die Daten des letzten Frames wiederverwenden, um die Framerate hoch zu halten. Visuell sieht das etwas ruckelig aus, aber der große Unterschied besteht darin, dass der Browser weiterhin auf Nutzereingaben reagiert.

Fazit

AnimationWorklet hat viele Facetten und bietet viele Vorteile für das Web. Die offensichtlichen Vorteile sind eine bessere Kontrolle über Animationen und neue Möglichkeiten, Animationen zu optimieren, um die visuelle Treue im Web auf ein neues Niveau zu heben. Mit dem API-Design können Sie Ihre App aber auch widerstandsfähiger gegen Ruckler machen und gleichzeitig auf alle neuen Funktionen zugreifen.

Das Animation Worklet ist in Canary verfügbar und wir planen einen Ursprungstest mit Chrome 71. Wir freuen uns auf Ihre neuen Weberfahrungen und darauf, von Ihnen zu erfahren, was wir verbessern können. Es gibt auch einen Polyfill, der dieselbe API bietet, aber keine Leistungsisolierung.

Beachten Sie, dass CSS-Übergänge und CSS-Animationen weiterhin gute Optionen sind und für einfache Animationen viel einfacher sein können. Wenn Sie es aber etwas ausgefallener mögen, ist AnimationWorklet die richtige Wahl.