Animationen in Web-Apps optimieren
Kurz gesagt:Mit Animation Worklet können Sie imperative Animationen schreiben, die mit der nativen Framerate des Geräts ausgeführt werden, um eine besonders flüssige Darstellung zu erzielen. Außerdem sind Ihre Animationen weniger anfällig für Ruckeln im Hauptthread und können an den Scrollvorgang anstatt an die Zeit gebunden 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 bereits heute als progressive Verbesserung verwenden.
Eine weitere Animation API?
Nein, es ist eine Erweiterung des Bestehenden, und das aus gutem Grund. Fangen wir noch einmal von vorn an. Wenn Sie heute ein beliebiges DOM-Element im Web animieren möchten, haben Sie 2 ½ 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 fast beliebig komplexe Animationen. Die WAAPI-Unterstützungsmatrix sieht ziemlich düster aus, aber es geht aufwärts. Bis dahin gibt es ein Polyfill.
Alle diese Methoden sind zustandslos und zeitabhängig. Einige der Effekte, die Entwickler ausprobieren, sind jedoch weder zeitabhängig noch zustandslos. Der berüchtigte Parallax-Scroller wird beispielsweise, wie der Name schon sagt, durch Scrollen ausgelöst. Einen leistungsstarken Parallax-Scroller im Web zu implementieren, ist heute überraschend schwierig.
Und was ist mit der Zustandslosigkeit? Denken Sie beispielsweise an die Adressleiste von Chrome unter Android. Wenn Sie nach unten scrollen, wird es nicht mehr angezeigt. Sobald Sie jedoch nach oben scrollen, wird sie wieder angezeigt, auch wenn Sie sich auf der Hälfte der Seite befinden. Die Animation hängt nicht nur von der Scrollposition, sondern auch von der vorherigen Scrollrichtung ab. Sie ist zustandsorientiert.
Ein weiteres Problem ist das Stylen von Scrollbars. Sie lassen sich nur schwer oder gar nicht formatieren. Was ist, wenn ich eine Nyan Cat als Scrollleiste haben möchte? Egal, welche Technik Sie wählen, das Erstellen einer benutzerdefinierten Scrollleiste ist weder leistungsstark noch einfach.
All diese Dinge sind umständlich und lassen sich nur schwer oder gar nicht effizient umsetzen. Die meisten basieren auf Ereignissen und/oder requestAnimationFrame
. Dadurch kann es sein, dass die Bildwiederholrate bei 60 fps bleibt, obwohl dein Bildschirm 90 fps, 120 fps oder mehr unterstützt. Außerdem wird nur ein Bruchteil des wertvollen Frame-Budgets des Haupt-Threads verwendet.
Animation Worklet erweitert die Möglichkeiten des Animationsstacks des Webs, um solche Effekte zu vereinfachen. Bevor wir loslegen, sollten wir uns noch einmal die Grundlagen von Animationen ansehen.
Einführung in Animationen und Zeitachsen
WAAPI und Animation Worklet verwenden Zeitachsen in großem Umfang, damit Sie Animationen und Effekte nach Bedarf steuern können. In diesem Abschnitt wird kurz wiederholt oder eingeführt, was Zeitachsen sind und wie sie mit Animationen funktionieren.
Jedes Dokument hat document.timeline
. Er beginnt bei 0, wenn das Dokument erstellt wird, und zählt die Millisekunden seit der Erstellung des Dokuments. Alle Animationen eines Dokuments werden relativ zu dieser Zeitachse ausgeführt.
Sehen wir uns zur Veranschaulichung diesen 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, wird für die Animation die currentTime
der Zeitachse als Startzeit verwendet. Unsere Animation hat eine Verzögerung von 3.000 ms. Das bedeutet, dass die Animation beginnt (oder „aktiv“ wird), wenn die Zeitachse `startTime` erreicht.
- 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`. Die Zeitachse steuert also, an welcher Stelle der Animation wir uns befinden.
Sobald die Animation den letzten Keyframe erreicht hat, wird sie zum ersten Keyframe zurückgesetzt und die nächste Iteration der Animation beginnt. Dieser Vorgang wird insgesamt dreimal wiederholt, da wir iterations: 3
festgelegt haben. Wenn die Animation nie enden soll, schreiben wir iterations: Number.POSITIVE_INFINITY
. Hier ist das Ergebnis des oben stehenden Codes.
WAAPI ist unglaublich leistungsstark und bietet viele weitere Funktionen wie Easing, Start-Offsets, Keyframe-Gewichtungen und Fill-Verhalten, die den Rahmen dieses Artikels sprengen würden. Wenn Sie mehr erfahren möchten, empfehle ich Ihnen diesen Artikel zu CSS-Animationen auf CSS Tricks.
Animation Worklet schreiben
Nachdem wir uns nun mit dem Konzept von Zeitachsen vertraut gemacht haben, können wir uns Animation Worklet ansehen und wie Sie damit Zeitachsen bearbeiten können. Die Animation Worklet API basiert nicht nur auf WAAPI, sondern ist im Sinne des erweiterbaren Webs ein Low-Level-Primitive, das die Funktionsweise von WAAPI erklärt. Die Syntax ist 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 liegt im ersten Parameter, dem Namen des Worklets, das diese Animation steuert.
Funktionserkennung
Chrome ist der erste Browser, in dem diese Funktion verfügbar ist. Sie müssen also darauf achten, dass Ihr Code nicht nur AnimationWorklet
erwartet. Bevor wir das Worklet laden, sollten wir also mit einem einfachen Check prüfen, 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 die Entwicklung und Skalierung vieler neuer APIs zu vereinfachen. Wir werden später noch genauer auf Worklets eingehen. Der Einfachheit halber können Sie sich Worklets vorerst als kostengünstige und einfache Threads (wie Worker) vorstellen.
Wir müssen dafür sorgen, dass ein Worklet mit dem Namen „passthrough“ geladen wurde, 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 passiert hier? Wir registrieren eine Klasse als Animator mit dem registerAnimator()
-Aufruf von AnimationWorklet 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 Promise 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 die currentTime
der Zeitachse der Animation sowie der Effekt, der gerade verarbeitet wird, übergeben. Wir haben nur einen Effekt, 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 Sie in der Demo sehen können.
Zeit
Der Parameter currentTime
unserer Methode animate()
ist der currentTime
der Zeitachse, die wir an den Konstruktor WorkletAnimation()
übergeben haben. Im vorherigen Beispiel haben wir die Zeit einfach an den Effekt übergeben. Da es sich aber um JavaScript-Code handelt, können wir die 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()
der currentTime
und ordnen diesen Wert dem Bereich [0; 2000] zu, für den unser Effekt definiert ist. Jetzt sieht die Animation ganz anders aus, ohne dass wir die Keyframes oder die Optionen der Animation geändert haben. Der Worklet-Code kann beliebig komplex sein. Sie können damit programmatisch festlegen, welche Effekte in welcher Reihenfolge und in welchem Umfang wiedergegeben werden.
Optionen über Optionen
Möglicherweise möchten Sie ein Worklet wiederverwenden und die Zahlen darin ändern. Aus diesem Grund können Sie dem WorkletAnimation-Konstruktor ein Optionenobjekt ü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 gesteuert.
Gib mir dein Bundesland!
Wie bereits angedeutet, ist eines der Hauptprobleme, die mit Animation Worklet gelöst werden sollen, die Unterstützung von zustandsbehafteten Animationen. Animations-Worklets dürfen einen Status haben. Eine der Kernfunktionen von Worklets ist jedoch, dass sie in einen anderen Thread migriert oder sogar zerstört werden können, um Ressourcen zu sparen. Dadurch würde auch ihr Status zerstört. Um den Verlust von Status zu verhindern, bietet das Animations-Worklet einen Hook, der vor dem Löschen eines Worklets aufgerufen wird und mit dem Sie ein Statusobjekt zurückgeben können. Dieses Objekt wird an den Konstruktor übergeben, wenn das Worklet neu erstellt wird. Bei der Erstellung ist 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-Chance, in welche Richtung sich das Quadrat dreht. Wenn der Browser das Worklet beenden und zu einem anderen Thread migrieren würde, gäbe es bei der Erstellung einen weiteren Math.random()
-Aufruf, der eine plötzliche Richtungsänderung verursachen könnte. Damit das nicht passiert, geben wir die zufällig ausgewählte Richtung der Animationen als state zurück und verwenden sie im Konstruktor, sofern angegeben.
ScrollTimeline: Das Raum-Zeit-Kontinuum nutzen
Wie im vorherigen Abschnitt gezeigt, können wir mit AnimationWorklet programmatisch definieren, wie sich das Vorrücken der Zeitachse auf die Effekte der Animation auswirkt. Bisher war unsere Zeitachse jedoch immer document.timeline
, die die Zeit erfasst.
ScrollTimeline
eröffnet neue Möglichkeiten und ermöglicht es Ihnen, Animationen mit Scrollen statt mit der Zeit zu steuern. Wir verwenden für diese Demo unser erstes „Passthrough“-Worklet wieder:
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();
Anstelle von document.timeline
erstellen wir eine neue ScrollTimeline
.
Wie Sie vielleicht schon vermutet haben, wird bei ScrollTimeline
nicht die Zeit, sondern die Scrollposition des scrollSource
verwendet, um den currentTime
im Worklet festzulegen. Wenn Sie ganz nach oben (oder links) gescrollt haben, ist currentTime = 0
. Wenn Sie ganz nach unten (oder rechts) gescrollt haben, wird currentTime
auf timeRange
festgelegt. Wenn Sie in dieser Demo im Feld scrollen, können Sie die Position des roten Felds steuern.
Wenn Sie ein ScrollTimeline
mit einem Element erstellen, das nicht gescrollt wird, ist die currentTime
der Zeitachse NaN
. Gerade beim responsiven Design sollten Sie also immer mit NaN
als currentTime
rechnen. Häufig ist es sinnvoll, standardmäßig den Wert 0 zu verwenden.
Das Verknüpfen von Animationen mit der Scrollposition ist etwas, das schon lange gewünscht wurde, aber bisher nicht in dieser Detailtiefe erreicht wurde (abgesehen von umständlichen Workarounds mit CSS3D). Mit Animation Worklet lassen sich diese Effekte auf einfache Weise und mit hoher Leistung implementieren. Beispiel: Ein Parallax-Scrolling-Effekt wie in dieser Demo zeigt, dass es jetzt nur noch ein paar Zeilen braucht, um eine scrollgesteuerte Animation zu definieren.
Funktionsweise
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 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 wichtig für AnimationWorklet.
Compositor NSync
Sie wissen vielleicht, dass sich bestimmte CSS-Eigenschaften schnell animieren lassen, andere nicht. Bei einigen Eigenschaften muss nur die GPU animiert werden, bei anderen muss der Browser das gesamte Dokument neu rendern.
In Chrome (wie in vielen anderen Browsern) gibt es einen Prozess namens „Compositor“, dessen Aufgabe es ist, 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-Properties animiert werden, muss der Browser möglicherweise nur den Compositor seine Arbeit erledigen lassen, während für andere Properties das Layout ausgeführt werden muss. Dies ist ein Vorgang, der nur vom Hauptthread ausgeführt werden kann. Je nachdem, welche Properties Sie animieren möchten, wird Ihr Animation-Worklet entweder an den Hauptthread gebunden oder in einem separaten Thread synchron mit dem Compositor ausgeführt.
Leichte Strafe
In der Regel gibt es nur einen Compositor-Prozess, der möglicherweise von mehreren 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 also, 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äß Spezifikation „verrutschen“. Er fällt hinter dem Compositor zurück und der Compositor darf die Daten des letzten Frames wiederverwenden, um die Framerate aufrechtzuerhalten. Visuell sieht das wie Ruckeln aus, aber der große Unterschied besteht darin, dass der Browser weiterhin auf Nutzereingaben reagiert.
Fazit
AnimationWorklet hat viele Facetten und bietet dem Web viele Vorteile. Die offensichtlichen Vorteile sind mehr Kontrolle über Animationen und neue Möglichkeiten, Animationen zu steuern, um dem Web ein neues Maß an visueller Qualität zu verleihen. Das API-Design ermöglicht es Ihnen aber auch, Ihre App widerstandsfähiger gegen Ruckeln zu machen und gleichzeitig auf alle neuen Funktionen zuzugreifen.
Animation Worklet ist in Canary verfügbar und wir planen einen Ursprungstest mit Chrome 71. Wir freuen uns auf Ihre neuen Web-Erlebnisse und darauf, von Ihnen zu hören, was wir verbessern können. Es gibt auch ein Polyfill, das dieselbe API bietet, aber keine Leistungsisolation.
CSS-Übergänge und CSS-Animationen sind weiterhin gültige Optionen und können für einfache Animationen viel einfacher sein. Wenn Sie jedoch etwas Besonderes benötigen, ist AnimationWorklet genau das Richtige für Sie.