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 befindet sich in Chrome Canary (hinter dem Flag "Experimental Web Platform features" (Experimentelle Webplattform-Funktionen) und 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 zwei Möglichkeiten: CSS-Übergänge für einfache A-B-Übergänge, CSS-Animationen für potenziell zyklische, komplexere zeitbasierte Animationen und die Web Animations API (WAAPI) für nahezu beliebig komplexe Animationen. Die Unterstützungsmatrix von WAAPI sieht ziemlich düster aus, ist aber in Vorbereitung. 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 leistungsfähigen Parallaxe-Scrollers im Web ist heute erstaunlich 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. Wie gehe ich vor, wenn eine Nyankatze als Bildlaufleiste verwendet werden soll? Für welches Verfahren Sie sich auch entscheiden, das Erstellen einer benutzerdefinierten Bildlaufleiste ist 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 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 zur Verdeutlichung 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

  • 3.000. 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 + 3.000 + 1000and the last keyframe atstartTime + 3.000 + 2000“. Punkt ist, die Zeitachse bestimmt, wo wir uns in unserer Animation befinden.

Sobald die Animation den letzten Keyframe erreicht hat, springt sie zum ersten Keyframe zurück 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 also 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.

Bevor Sie die Animation deklarieren, müssen Sie sicherstellen, dass ein Worklet mit dem Namen "Passthrough" geladen wurde:

// 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 der 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 mit der Erstellung von Animationen mit diesem Worklet beginnen.

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 wir verwenden currentTime, um den localTime des Effekts festzulegen. Daher wird dieser Animationsfilm „Passthrough“ genannt. 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 Ihnen, programmatisch zu definieren, welche Effekte in welcher Reihenfolge und in welchem Umfang gespielt 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 dein Bundesland.

Wie bereits erwähnt, soll mit dem Animation Worklet vor allem ein Problem gelöst werden: zustandsabhängige Animationen. Animations-Worklets dürfen einen Status speichern. Eines der Kernmerkmale von Worklets besteht jedoch darin, dass sie in einen anderen Thread migriert oder sogar gelöscht werden können, um Ressourcen zu sparen, was auch ihren Status löschen würde. 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 löscht und zu einem anderen Thread migriert, kommt es beim Erstellen zu einem weiteren Math.random()-Aufruf, der zu einer plötzlichen Richtungsänderung führen könnte. 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 Sie mit AnimationWorklet programmatisch definieren, wie sich das Verschieben der Zeitachse 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 zu übergeben, 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 ganz nach oben (oder nach links) gescrollt wird, bedeutet das currentTime = 0, während currentTime ganz nach unten (oder nach rechts) gescrollt wird, wird der Wert 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.

Das Verknüpfen von Animationen mit der Scrollposition wurde schon lange erhofft, wurde aber nie wirklich so realistisch wie möglich umgesetzt (abgesehen von komplizierten Behelfslösungen mit CSS3D). Mit dem Animation Worklet können diese Effekte unkompliziert implementiert werden, ohne dass dabei eine hohe Leistung erzielt wird. Beispiel: Mit einem Paralax-Scrolling-Effekt wie in dieser Demo lässt sich eine scrollbare 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 über 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. Dies ist besonders für AnimationWorklet wichtig.

Compositor-NSync

Sie wissen vielleicht, dass bestimmte CSS-Eigenschaften schnell animiert werden können, andere aber 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.

Auf das Handgelenk schlagen

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, bricht der gesamte Browser zum Stillstand und reagiert nicht mehr auf Nutzereingaben. Dies muss um jeden Preis 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 mehr Kontrolle über Animationen und neue Möglichkeiten zum Generieren von Animationen, um dem Web eine neue Ebene visueller Fidelity zu verleihen. Das API-Design ermöglicht es Ihnen aber auch, Ihre Anwendung ausfallsicherer zu machen und gleichzeitig Zugriff auf alle neuen Funktionen zu erhalten.

Animation Worklet befindet sich in Canary und wir planen einen Ursprungstest mit Chrome 71. Wir freuen uns schon auf Ihre tollen neuen Erfahrungen und Verbesserungsvorschläge. Es gibt auch einen Polyfill, mit dem Sie dieselbe API, aber nicht die Leistungsisolierung erhalten.

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