Einführung
Ihre Webanwendung sollte bei Animationen, Übergängen und anderen kleinen UI-Effekten reaktionsschnell und flüssig laufen. Wenn Sie dafür sorgen, dass diese Effekte flüssig ablaufen, kann das den Unterschied zwischen einem „nativen“ Gefühl und einem klobigen, unpolierten Gefühl ausmachen.
Dies ist der erste Artikel in einer Reihe, in der die Optimierung der Renderingleistung im Browser behandelt wird. Zuerst sehen wir uns an, warum flüssige Animationen schwierig sind und was dafür getan werden muss. Außerdem lernen Sie einige einfache Best Practices kennen. Viele dieser Ideen wurden ursprünglich in „Jank Busters“ vorgestellt, einem Vortrag, den Nat Duca und ich dieses Jahr auf der Google I/O gehalten haben (Video).
V-Sync
PC-Gamern ist dieser Begriff vielleicht geläufig, im Web ist er jedoch eher selten: Was ist V-Sync?
Denken Sie an das Display Ihres Smartphones: Es wird in regelmäßigen Abständen aktualisiert, normalerweise (aber nicht immer!) etwa 60 Mal pro Sekunde. Die vertikale Synchronisierung (V-Sync) bezieht sich auf die Praxis, neue Frames nur zwischen den Bildschirmaktualisierungen zu generieren. Sie können sich das als eine Art Wettlauf zwischen dem Prozess vorstellen, der Daten in den Bildschirmpuffer schreibt, und dem Betriebssystem, das diese Daten liest, um sie auf dem Display anzuzeigen. Der Inhalt des gepufferten Frames soll sich zwischen diesen Aktualisierungen ändern, nicht währenddessen. Andernfalls zeigt der Monitor die Hälfte eines Frames und die Hälfte eines anderen an, was zu Tearing führt.
Für eine flüssige Animation muss bei jeder Bildschirmaktualisierung ein neuer Frame bereit sein. Das hat zwei große Auswirkungen: Frame-Timing (d. h., wann der Frame fertig sein muss) und Frame-Budget (d. h., wie lange der Browser Zeit hat, einen Frame zu erstellen). Sie haben nur die Zeit zwischen den Bildschirmaktualisierungen, um einen Frame fertigzustellen (ca. 16 ms bei einem 60-Hz-Bildschirm). Sie möchten mit der Produktion des nächsten Frames beginnen, sobald der letzte auf dem Bildschirm angezeigt wurde.
Timing ist alles: requestAnimationFrame
Viele Webentwickler verwenden setInterval
oder setTimeout
alle 16 Millisekunden, um Animationen zu erstellen. Das ist aus verschiedenen Gründen problematisch (mehr dazu gleich), aber besonders wichtig sind:
- Die Timerauflösung von JavaScript liegt nur im Bereich weniger Millisekunden.
- Unterschiedliche Geräte haben unterschiedliche Bildwiederholraten
Denken Sie an das oben erwähnte Problem mit dem Frame-Timing: Sie benötigen einen vollständigen Animationsframe, der alle JavaScript-, DOM-Manipulations-, Layout-, Mal- und anderen Prozesse abgeschlossen hat, bevor die nächste Bildschirmaktualisierung erfolgt. Eine niedrige Timerauflösung kann es erschweren, Animationsframes vor der nächsten Bildschirmaktualisierung fertigzustellen. Bei unterschiedlichen Bildschirmaktualisierungsraten ist dies mit einem festen Timer jedoch unmöglich. Unabhängig vom Timerintervall weichen Sie langsam aus dem Zeitfenster für einen Frame heraus und verlieren ihn. Das würde auch passieren, wenn der Timer mit einer Millisekundengenauigkeit ausgelöst würde, was er aber nicht tut (wie Entwickler festgestellt haben). Die Timerauflösung variiert je nachdem, ob der Computer mit Akku oder an der Stromversorgung angeschlossen ist, und kann von Tabs im Hintergrund beeinflusst werden, die Ressourcen belegen. Auch wenn das selten vorkommt (z. B. alle 16 Frames, weil Sie um eine Millisekunde daneben lagen), werden Sie es bemerken: Sie verlieren mehrere Frames pro Sekunde. Außerdem müssen Sie Frames generieren, die nie angezeigt werden. Das verschwendet Strom und CPU-Zeit, die Sie für andere Dinge in Ihrer Anwendung verwenden könnten.
Unterschiedliche Displays haben unterschiedliche Bildwiederholraten: 60 Hz ist üblich, aber einige Smartphones haben 59 Hz, einige Laptops sinken im Energiesparmodus auf 50 Hz und einige Desktopmonitore haben 70 Hz.
Wenn es um die Renderingleistung geht, konzentrieren wir uns in der Regel auf Frames pro Sekunde (FPS). Die Abweichung kann jedoch ein noch größeres Problem sein. Unsere Augen bemerken die winzigen, unregelmäßigen Ruckler in der Animation, die eine schlecht getaktete Animation verursachen kann.
Mit requestAnimationFrame
können Sie Frames mit der richtigen Zeitstempelung erstellen. Wenn Sie diese API verwenden, fragen Sie den Browser nach einem Animationsframe. Ihr Callback wird aufgerufen, wenn der Browser bald einen neuen Frame generiert. Das passiert unabhängig von der Bildwiederholrate.
requestAnimationFrame
hat noch weitere Vorteile:
- Animationen in Tabs im Hintergrund werden pausiert, um Systemressourcen und Akkulaufzeit zu schonen.
- Wenn das System das Rendering nicht mit der Bildwiederholrate des Displays verarbeiten kann, kann es Animationen drosseln und den Rückruf seltener ausführen (z. B. 30 Mal pro Sekunde auf einem 60-Hz-Display). Dadurch wird die Framerate zwar halbiert, die Animation bleibt aber einheitlich. Wie bereits erwähnt, sind unsere Augen viel empfindlicher für Schwankungen als für die Framerate. Eine stabile Bildrate von 30 Hz sieht besser aus als 60 Hz, bei denen einige Frames pro Sekunde fehlen.
requestAnimationFrame
wurde bereits vielfach diskutiert. Weitere Informationen finden Sie in Artikeln wie diesem von Creative JS. Es ist jedoch ein wichtiger erster Schritt zu einer flüssigen Animation.
Frame-Budget
Da wir bei jeder Bildschirmaktualisierung einen neuen Frame bereitstellen möchten, bleibt nur die Zeit zwischen den Aktualisierungen, um einen neuen Frame zu erstellen. Bei einem 60-Hz-Display haben wir also etwa 16 ms Zeit, um das gesamte JavaScript auszuführen, das Layout zu erstellen, zu zeichnen und alles andere zu tun, was der Browser tun muss, um den Frame auszugeben. Wenn das JavaScript in Ihrem requestAnimationFrame
-Callback also länger als 16 ms läuft, können Sie keinen Frame rechtzeitig für die V‑Synchronisierung erstellen.
16 ms sind nicht viel Zeit. Glücklicherweise können Sie mit den Entwicklertools von Chrome herausfinden, ob Sie Ihr Frame-Budget während des requestAnimationFrame-Callbacks überschreiten.
Wenn wir die Zeitleiste der Dev-Tools öffnen und eine Aufzeichnung dieser Animation in Aktion erstellen, wird schnell klar, dass wir beim Animieren weit über dem Budget liegen. Wechsele in der Zeitleiste zu „Frames“ und sieh dir das an:
Diese requestAnimationFrame-Callbacks (rAF) dauern länger als 200 ms. Das ist um eine Größenordnung zu lang, um alle 16 ms einen Frame zu rendern. Wenn Sie einen dieser langen RAF-Callbacks öffnen, sehen Sie, was darin passiert: in diesem Fall viel Layout.
In seinem Video geht Paul genauer auf die Ursache für das Neulayout (scrollTop
wird angezeigt) und darauf ein, wie Sie es vermeiden können. Der Punkt ist aber, dass Sie den Rückruf untersuchen und herausfinden können, warum er so lange dauert.
Beachten Sie die Frame-Zeiten von 16 ms. Dieser leere Raum in den Frames ist der Spielraum, den Sie haben, um mehr zu arbeiten (oder den Browser im Hintergrund arbeiten zu lassen). Dieser Negativraum ist gut.
Andere Quelle von Ruckeln
Die größte Ursache für Probleme beim Ausführen von JavaScript-basierten Animationen ist, dass andere Dinge den rAF-Callback beeinträchtigen und sogar verhindern können, dass er ausgeführt wird. Auch wenn Ihr RAF-Callback effizient ist und nur wenige Millisekunden dauert, können andere Aktivitäten (z. B. die Verarbeitung einer gerade eingegangenen XHR, die Ausführung von Eingabeereignis-Handlern oder die Ausführung geplanter Updates über einen Timer) plötzlich auftreten und beliebig lange ausgeführt werden, ohne dass ein Ergebnis ausgegeben wird. Auf Mobilgeräten kann die Verarbeitung dieser Ereignisse manchmal Hunderte von Millisekunden dauern, während derer die Animation vollständig pausiert. Wir nennen diese Ruckler Jank.
Es gibt keine Zauberformel, um diese Situationen zu vermeiden, aber es gibt einige Best Practices für die Architektur, mit denen Sie sich auf Erfolg vorbereiten können:
- Führen Sie in Eingabe-Handlern nicht zu viel Verarbeitung durch. Wenn Sie viel JS verwenden oder versuchen, die gesamte Seite beispielsweise über einen Onscroll-Handler neu anzuordnen, ist das eine sehr häufige Ursache für Ruckler.
- Verschieben Sie so viel Verarbeitung wie möglich (d. h. alles, was lange dauert) in Ihren RAF-Callback oder Webworker.
- Wenn Sie Arbeit in den RAF-Callback schieben, versuchen Sie, sie in kleinere Teile zu unterteilen, damit Sie jeden Frame nur ein wenig verarbeiten. Sie können die Arbeit auch bis nach einer wichtigen Animation verzögern. So können Sie weiterhin kurze RAF-Callbacks ausführen und flüssig animieren.
Eine gute Anleitung dazu, wie Sie die Verarbeitung in requestAnimationFrame-Callbacks statt in Input-Handler schieben, finden Sie im Artikel Leaner, Meaner, Faster Animations with requestAnimationFrame von Paul Lewis.
CSS-Animation
Was ist besser als schlankes JS in Ihren Ereignis- und RAF-Callbacks? Kein JS.
Wir haben bereits erwähnt, dass es keine Patentlösung gibt, um Unterbrechungen von RAF-Callbacks zu vermeiden. Mit CSS-Animationen können Sie sie jedoch vollständig vermeiden. Insbesondere in Chrome für Android (und andere Browser arbeiten an ähnlichen Funktionen) haben CSS-Animationen die sehr wünschenswerte Eigenschaft, dass der Browser sie oft auch dann ausführen kann, wenn JavaScript aktiv ist.
Im Abschnitt zu Rucklern wurde bereits implizit erwähnt, dass Browser nur eine Sache gleichzeitig tun können. Das ist zwar nicht ganz richtig, aber eine gute Arbeitsannahme: Der Browser kann jederzeit JS ausführen, das Layout erstellen oder malen, aber immer nur eine dieser Aktionen. Das lässt sich in der Zeitachse in den Dev-Tools überprüfen. Eine Ausnahme von dieser Regel sind CSS-Animationen in Chrome für Android (und bald auch in Chrome für Computer).
Wenn möglich, sollten Sie eine CSS-Animation verwenden, da dies Ihre Anwendung vereinfacht und die Animationen auch bei laufender JavaScript-Ausführung flüssig laufen.
// see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for info on rAF polyfills
rAF = window.requestAnimationFrame;
var degrees = 0;
function update(timestamp) {
document.querySelector('#foo').style.webkitTransform = "rotate(" + degrees + "deg)";
console.log('updated to degrees ' + degrees);
degrees = degrees + 1;
rAF(update);
}
rAF(update);
Wenn Sie auf die Schaltfläche klicken, wird JavaScript 180 ms lang ausgeführt, was zu Rucklern führt. Wenn wir diese Animation jedoch mit CSS-Animationen ausführen, tritt das Ruckeln nicht mehr auf.
Zur Erinnerung: Zum Zeitpunkt der Erstellung dieses Artikels sind CSS-Animationen nur in Chrome für Android flüssig, nicht in der Desktopversion von Chrome.
/* tools like Modernizr (http://modernizr.com/) can help with CSS polyfills */
#foo {
+animation-duration: 3s;
+animation-timing-function: linear;
+animation-animation-iteration-count: infinite;
+animation-animation-name: rotate;
}
@+keyframes: rotate; {
from {
+transform: rotate(0deg);
}
to {
+transform: rotate(360deg);
}
}
Weitere Informationen zur Verwendung von CSS-Animationen finden Sie in Artikeln wie diesem auf MDN.
Zusammenfassung
Kurz gesagt:
- Bei der Animation ist es wichtig, Frames für jede Bildschirmaktualisierung zu erstellen. Die Synchronisierung der Animation mit der Bildwiederholrate hat einen großen positiven Einfluss auf die Nutzerfreundlichkeit einer App.
- Die beste Möglichkeit, eine vsync-synchronisierte Animation in Chrome und anderen modernen Browsern zu erzielen, ist die Verwendung von CSS-Animationen. Wenn Sie mehr Flexibilität als bei einer CSS-Animation benötigen, ist die requestAnimationFrame-basierte Animation die beste Methode.
- Damit RAF-Animationen reibungslos funktionieren, sollten andere Ereignishandler nicht den Ablauf des RAF-Callbacks beeinträchtigen. Außerdem sollten RAF-Callbacks kurz sein (< 15 ms).
Außerdem gilt die vsync-synchronisierte Animation nicht nur für einfache UI-Animationen, sondern auch für Canvas2D-Animationen, WebGL-Animationen und sogar für das Scrollen auf statischen Seiten. Im nächsten Artikel dieser Reihe gehen wir anhand dieser Konzepte auf die Scrollleistung ein.
Viel Spaß beim Animieren!