Animations-Worklet von Houdini

Animationen Ihrer Webanwendung optimieren

TL;DR:Mit dem Animations-Worklet können Sie imperative Animationen schreiben, die mit der nativen Framerate des Geräts ausgeführt werden. Sie sorgen für eine zusätzliche ruckelfreie SmoothnessTM. Ihre Animationen sind dadurch widerstandsfähiger gegen eine Verzögerung des Hauptthreads und können verlinkbar sein, um ohne Zeitverlust zu scrollen. Das Animations-Worklet befindet sich in Chrome Canary (hinter dem Flag „Experimental Web Platform features“). Wir planen einen Ursprungstest für Chrome 71. Du kannst es heute als progressive Verbesserung verwenden.

Noch eine Animations-API?

Nein, es ist eine Erweiterung dessen, was wir bereits haben, und das aus gutem Grund! Fangen wir am Anfang an. Wenn Sie ein beliebiges DOM-Element im Web animieren möchten, haben Sie zwei Hälften Möglichkeiten: CSS-Übergänge für einfache A-zu-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 WAAPI-Supportmatrix sieht ziemlich düster aus, aber sie ist auf dem besten Weg. Bis dahin gibt es polyfill.

Allen diesen Methoden ist es gemeinsam, dass sie zustandslos und zeitgesteuert sind. Einige der Effekte, mit denen Entwickler versuchen, sind weder zeitgesteuert noch zustandslos. Der berüchtigte Parallaxe-Scroller ist z. B. scrollbar, wie der Name schon sagt. Ein leistungsfähiges Parallaxe-Scroller im Web zu implementieren, ist erstaunlich schwierig.

Und was ist mit Zustandslosigkeit? Nehmen wir z. B. die Adressleiste von Chrome unter Android. Wenn Sie nach unten scrollen, wird sie aus dem sichtbaren Bereich gescrollt. Aber wenn Sie nach oben scrollen, erscheint sie wieder, auch wenn Sie sich auf der Hälfte der Seite weiter unten befinden. Die Animation hängt nicht nur von der Scrollposition ab, sondern auch von der bisherigen Scrollrichtung. Sie ist zustandsorientiert.

Ein weiteres Problem sind die Gestaltung von Bildlaufleisten. Sie sind bekanntermaßen nicht stilierbar – oder zumindest nicht stilfähig genug. Was ist, wenn ich eine Nyan-Katze als Bildlaufleiste haben möchte? Ganz gleich, für welches Verfahren Sie sich entscheiden: Das Erstellen einer benutzerdefinierten Bildlaufleiste ist weder leistungsfähig noch einfach.

Wichtig ist, dass all diese Dinge umständlich und schwer bis unmöglich zu implementieren sind. Die meisten davon basieren auf Ereignissen und/oder requestAnimationFrame, bei denen Sie möglicherweise bei 60 fps bleiben, selbst wenn Ihr Bildschirm mit 90 fps, 120 fps oder mehr laufen kann und nur einen Bruchteil Ihres wertvollen Main-Thread-Frame-Budgets verbraucht.

Animation Worklet erweitert die Funktionen des Animationsstacks im Web, um diese Art von Effekten zu vereinfachen. Sehen wir uns zunächst die Grundlagen von Animationen an.

Eine Einführung zu Animationen und Zeitachsen

WAAPI und Animation Worklet nutzen in großem Umfang Zeitachsen, damit Sie Animationen und Effekte nach Ihren Wünschen orchestrieren können. Dieser Abschnitt ist eine schnelle Auffrischung oder Einführung in Zeitpläne und deren Funktionsweise mit Animationen.

Jedes Dokument hat document.timeline. Der Wert beginnt bei 0, wenn das Dokument erstellt wird, und zählt die Millisekunden seit dem Start des Dokuments. Alle Animationen eines Dokuments wirken sich auf diese Zeitachse aus.

Sehen wir uns zur Veranschaulichung 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 animation.play() aufgerufen wird, verwendet die Animation den currentTime der Zeitachse als Startzeit. Unsere Animation hat eine Verzögerung von 3.000 ms. Das bedeutet, dass die Animation startet (oder "aktiv" wird), wenn für die Zeitachse "startTime" erreicht wird.

  • 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 + 3000 + 2000. Entscheidend ist, dass die Zeitachse 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. Seit iterations: 3 wurde dieser Vorgang insgesamt 3-mal wiederholt. Wenn die Animation niemals beendet werden soll, schreiben Sie iterations: Number.POSITIVE_INFINITY. Hier ist das Ergebnis des obigen Codes.

WAAPI ist unglaublich leistungsstark und es gibt viele weitere Funktionen in dieser API, wie Easing, Startversatz, Keyframe-Gewichtungen und Füllverhalten, die in diesem Artikel nicht behandelt werden. Weitere Informationen finden Sie in diesem Artikel zu CSS-Animationen auf CSS-Tricks.

Animations-Worklet schreiben

Jetzt kennen Sie das Konzept der Zeitachsen. Sehen wir uns nun das Animations-Worklet an und wie Sie damit Zeitachsen durcheinanderbringen können. Die Animation Worklet API basiert nicht nur auf WAAPI, sondern ist – im Sinne des extensiblen Web – eine untergeordnete Primitive, die die Funktionsweise von WAAPI erläutert. Was die Syntax anbelangt, sind sie 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 angeboten wird. Achten Sie deshalb darauf, dass Ihr Code nicht einfach AnimationWorklet erwartet. Bevor wir also die Workstation 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 wird geladen

Worklets sind ein neues Konzept, das von der Houdini-Taskforce eingeführt wurde, um viele der neuen APIs einfacher erstellen und skalieren zu können. Auf die Einzelheiten von Worklets kommen wir etwas später. Der Einfachheit halber können Sie sie sich vorerst als günstige und einfache Threads (wie Worker) vorstellen.

Bevor wir die Animation deklarieren, muss ein Worklet mit dem Namen "Passthrough" geladen werden:

// 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 passiert hier? Wir registrieren eine Klasse als Animator über den registerAnimator()-Aufruf des AnimationWorklet. Sie erhält den Namen "Passthrough". Es ist der gleiche Name, den wir im obigen WorkletAnimation()-Konstruktor verwendet haben. Sobald die Registrierung abgeschlossen ist, wird das von addModule() zurückgegebene Versprechen aufgelöst und wir können mit diesem Worklet Animationen erstellen.

Die animate()-Methode unserer Instanz wird für jeden Frame aufgerufen, den der Browser rendern möchte. Dabei werden der currentTime der Zeitachse der Animation sowie der gerade verarbeitete Effekt übergeben. Wir haben nur einen Effekt, den KeyframeEffect, und wir 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 Sie in der Demo sehen können.

Uhrzeit

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

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 die Math.sin() von currentTime und ordnen diesen Wert dem Bereich [0; 2000] neu zu. Dies ist der Zeitraum, für den unser Effekt definiert ist. Jetzt sieht die Animation ganz anders aus, ohne dass die Keyframes oder Animationsoptionen geändert wurden. Der Worklet-Code kann beliebig komplex sein und ermöglicht es Ihnen, programmatisch zu definieren, welche Effekte in welcher Reihenfolge und in welchem Umfang wiedergegeben werden.

Optionen statt Optionen

Sie können ein Worklet wiederverwenden und seine Nummern ändern. Aus diesem Grund ermöglicht Ihnen der WorkletAnimation-Konstruktor die Übergabe eines Optionsobjekts an das 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 diesem Beispiel werden beide Animationen mit demselben Code ausgeführt, aber mit unterschiedlichen Optionen.

Ermittle den Ort deines Landes!

Wie ich bereits angedeutet habe, sind zustandsorientierte Animationen eines der Hauptprobleme, das Animations-Worklet gelöst werden soll. Animations-Worklets dürfen den Status beibehalten. 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. Dadurch wird auch ihr Status gelöscht. Um einen Zustandsverlust zu vermeiden, bietet das Animations-Worklet einen Hook, der vor dem Löschen eines Worklets aufgerufen wird. Damit können Sie ein Statusobjekt zurückgeben. Dieses Objekt wird an den Konstruktor übergeben, wenn das Worklet neu erstellt wird. Bei der ersten Erstellung lautet dieser Parameter 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-Wahrscheinlichkeit, in welche Richtung sich das Quadrat dreht. Wenn der Browser das Worklet herunterfährt und in einen anderen Thread migriert, kommt es beim Erstellen zu einem weiteren Math.random()-Aufruf, was zu einer plötzlichen Richtungsänderung führen kann. Um dies zu verhindern, geben wir die zufällig ausgewählten Animationen als state zurück und verwenden ihn im Konstruktor, falls angegeben.

Ein Blick in das Raum-Zeit-Kontinuum: ScrollTimeline

Wie im vorherigen Abschnitt gezeigt, können wir mit AnimationWorklet programmatisch definieren, wie sich eine Erweiterung der Zeitachse auf die Auswirkungen der Animation auswirkt. Bisher war unsere Zeitachse immer document.timeline, die die Zeit verfolgt.

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

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 ein neues ScrollTimeline. Sie haben es vielleicht schon erraten: ScrollTimeline verwendet keine Zeit, sondern verwendet die Scrollposition von scrollSource, um den currentTime im Worklet festzulegen. Wenn ganz nach oben (oder links) gescrollt wird, bedeutet dies currentTime = 0, während ein Scrollen ganz nach unten (oder rechts) bedeutet, dass currentTime auf timeRange gesetzt ist. Wenn du das Feld in dieser Demo scrollst, kannst du die Position des roten Felds ändern.

Wenn Sie eine ScrollTimeline mit einem Element erstellen, das nicht scrollt, ist der currentTime der Zeitachse NaN. Besonders im Hinblick auf responsives Webdesign sollten Sie immer auf NaN als currentTime vorbereitet sein. Es ist oft sinnvoll, den Standardwert 0 zu verwenden.

Das Verknüpfen von Animationen mit Scrollposition war schon lange begehrt, wurde aber mit dieser Genauigkeit nie wirklich erreicht (abgesehen von kniffligen Behelfslösungen mit CSS3D). Mit dem Animations-Worklet können diese Effekte auf einfache Weise und gleichzeitig mit hoher Leistung implementiert werden. Ein Beispiel: Ein Parallaxe-Scrolleffekt wie in dieser Demo zeigt, dass es jetzt nur noch wenige Zeilen braucht, um eine scrollbare Animation zu definieren.

Details

Worklets

Worklets sind JavaScript-Kontexte mit einem isolierten Bereich 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. Darüber hinaus sind Worklets nicht an eine bestimmte Ereignisschleife gebunden, können aber 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, andere dagegen nicht. Einige Eigenschaften erfordern nur ein wenig Arbeit an der GPU, um animiert zu werden, während andere den Browser dazu zwingen, das Layout des gesamten Dokuments neu zu gestalten.

In Chrome haben wir, wie in vielen anderen Browsern, einen Prozess namens Compositor, dessen Aufgabe er ist – und ich mache das hier sehr vereinfachen –, um 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 (in der Regel 60 Hz). Je nachdem, welche CSS-Eigenschaften animiert werden, muss der Browser möglicherweise nur den Compositor für seine Funktionsweise haben, während andere Eigenschaften das Layout ausführen müssen. Dieser Vorgang kann nur vom Hauptthread ausgeführt werden. Je nachdem, welche Eigenschaften Sie animieren möchten, wird Ihr Animations-Worklet entweder an den Hauptthread gebunden oder in einem separaten Thread synchron mit dem Compositor ausgeführt.

Klatsch ums Handgelenk

Normalerweise gibt es nur einen Compositor-Prozess, der potenziell von mehreren Tabs gemeinsam genutzt wird, da die GPU eine stark umkämpfte Ressource ist. Wenn der Compositor irgendwie blockiert wird, hält der gesamte Browser an und reagiert nicht mehr auf Nutzereingaben. Das muss um jeden Preis vermieden werden. Was passiert also, wenn Ihr Worklet die Daten, die der Compositor zum Rendern des Frames benötigt, nicht rechtzeitig liefern kann?

In diesem Fall darf das Worklet – laut Spezifikation – „ausrutschen“. Es fällt hinter dem Compositor zurück und darf die Daten des letzten Frames wiederverwenden, um die Framerate aufrechtzuerhalten. Optisch wirkt dies nach einer Verzögerung, aber der große Unterschied besteht darin, dass der Browser weiterhin auf Nutzereingaben reagiert.

Fazit

AnimationWorklet bietet viele Facetten und seine Vorteile für das Web. Die offensichtlichen Vorteile sind mehr Kontrolle über Animationen und neue Möglichkeiten zur Steuerung von Animationen, um das Web auf eine neue Ebene der visuellen Darstellung zu bringen. Mit dem API-Design können Sie Ihre Anwendung jedoch auch gegen Verzögerungen anpassen und gleichzeitig Zugriff auf die vielen neuen Vorteile erhalten.

Animation Worklet befindet sich auf Canary und wir planen einen Ursprungstest mit Chrome 71. Wir sind schon sehr gespannt auf Ihre tollen neuen Web-Erlebnisse und darauf, was wir verbessern können. Es gibt auch einen polyfill, der Ihnen dieselbe API, aber nicht die Leistungsisolation bietet.

CSS-Übergänge und CSS-Animationen sind weiterhin gültige Optionen und können für einfache Animationen wesentlich einfacher sein. Aber wenn Sie Lust haben, ist das AnimationWorklet genau das Richtige!