Einführung
Ihre Webanwendung soll bei Animationen, Übergängen und anderen kleinen UI-Effekten reaktionsschnell und flüssig wirken. Wenn diese Effekte ruckelfrei sind, kann das den Unterschied zwischen einem „nativen“ und einem klobigen, unfertigen Gefühl ausmachen.
Dies ist der erste in einer Reihe von Artikeln, die sich mit der Optimierung der Renderingleistung im Browser befassen. Zu Beginn sehen wir uns an, warum flüssige Animationen schwierig sind und was nötig ist, um sie zu erreichen. Außerdem stellen wir einige einfache Best Practices vor. 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).
Jetzt neu: V-Sync
PC-Spieler kennen diesen Begriff vielleicht, aber im Web ist er eher selten: Was ist V-Sync?
Das Display deines Smartphones wird in regelmäßigen Abständen aktualisiert, normalerweise (aber nicht immer) etwa 60‑mal pro Sekunde. V-Sync (oder vertikale Synchronisation) bezieht sich auf die Praxis, neue Frames nur zwischen Bildschirmaktualisierungen zu generieren. Sie können sich das wie eine Race-Bedingung zwischen dem Prozess, der Daten in den Bildschirmpuffer schreibt, und dem Betriebssystem vorstellen, das diese Daten liest, um sie auf dem Display anzuzeigen. Der Inhalt des Frame-Puffers sollte 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 wichtige Auswirkungen: Frame-Timing (also wann der Frame bereit sein muss) und Frame-Budget (also wie lange der Browser Zeit hat, einen Frame zu erstellen). Sie haben nur die Zeit zwischen den Bildschirmaktualisierungen, um einen Frame fertigzustellen (~16 ms auf einem 60-Hz-Bildschirm), und 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 alle 16 Millisekunden setInterval oder setTimeout, um Animationen zu erstellen. Das ist aus verschiedenen Gründen ein Problem (auf die wir gleich noch näher eingehen werden), aber besonders besorgniserregend sind:
- Die Timer-Auflösung von JavaScript liegt nur in der Größenordnung von mehreren Millisekunden.
- Verschiedene Geräte haben unterschiedliche Aktualisierungsraten
Erinnern Sie sich an das oben erwähnte Problem mit dem Frame-Timing: Sie benötigen einen vollständigen Animationsframe, der mit JavaScript, DOM-Manipulation, Layout, Painting usw. abgeschlossen ist, bevor die nächste Bildschirmaktualisierung erfolgt. Eine niedrige Timer-Auflösung kann es schwierig machen, Animationsframes vor der nächsten Bildschirmaktualisierung fertigzustellen. Die unterschiedlichen Bildschirmaktualisierungsraten machen dies jedoch mit einem festen Timer unmöglich. Unabhängig vom Timerintervall wird das Timingfenster für einen Frame langsam verlassen und ein Frame wird ausgelassen. Das würde auch dann passieren, wenn der Timer millisekundengenau ausgelöst würde, was er nicht tut (wie Entwickler festgestellt haben). Die Timer-Auflösung variiert je nachdem, ob das Gerät im Akkubetrieb oder am Stromnetz angeschlossen ist, und kann durch Hintergrund-Tabs beeinträchtigt werden, die Ressourcen beanspruchen usw. Selbst wenn dies selten vorkommt (z. B. alle 16 Frames, weil Sie um eine Millisekunde daneben lagen), werden Sie es bemerken: Sie werden mehrere Frames pro Sekunde verlieren. Außerdem werden Frames generiert, die nie angezeigt werden. Das verschwendet Energie und CPU-Zeit, die Sie für andere Dinge in Ihrer Anwendung nutzen könnten.
Verschiedene Displays haben unterschiedliche Aktualisierungsraten: 60 Hz ist üblich, aber einige Smartphones haben 59 Hz, einige Laptops fallen im Energiesparmodus auf 50 Hz und einige Desktopmonitore haben 70 Hz.
Wenn es um die Rendering-Leistung geht, konzentrieren wir uns meist auf die Bilder pro Sekunde (FPS). Die Varianz kann jedoch ein noch größeres Problem sein. Unsere Augen bemerken die winzigen, unregelmäßigen Ruckler, die durch eine schlecht getimte Animation entstehen können.
Mit requestAnimationFrame erhalten Sie Animationsframes mit dem richtigen Timing. Wenn Sie diese API verwenden, fordern Sie vom Browser einen Animationsframe an. Der Callback wird aufgerufen, wenn der Browser bald einen neuen Frame erzeugt. Das passiert unabhängig von der Aktualisierungsrate.
requestAnimationFrame hat auch andere nützliche Eigenschaften:
- Animationen in Hintergrund-Tabs werden pausiert, um Systemressourcen und Akkulaufzeit zu sparen.
- Wenn das System das Rendering mit der Aktualisierungsrate des Displays nicht bewältigen kann, kann es Animationen drosseln und den Callback seltener ausführen (z. B. 30‑mal pro Sekunde auf einem 60‑Hz-Display). Dadurch wird die Framerate zwar halbiert, die Animation bleibt jedoch konsistent. Wie bereits erwähnt, reagieren unsere Augen viel empfindlicher auf Abweichungen als auf die Framerate. Konstante 30 Hz sehen besser aus als 60 Hz, bei denen pro Sekunde ein paar Frames fehlen.
requestAnimationFrame wurde bereits an vielen Stellen diskutiert. Weitere Informationen finden Sie in Artikeln wie diesem von creative JS. Es ist jedoch ein wichtiger erster Schritt für flüssige Animationen.
Frame-Budget
Da wir bei jeder Bildschirmaktualisierung einen neuen Frame bereitstellen möchten, bleibt nur die Zeit zwischen den Aktualisierungen, um alle Arbeiten zum Erstellen eines neuen Frames auszuführen. Bei einem 60-Hz-Display bedeutet das, dass wir etwa 16 ms Zeit haben, um den gesamten JavaScript-Code auszuführen, das Layout zu erstellen, die Seite zu rendern und alles andere zu tun, was der Browser tun muss, um den Frame auszugeben. Wenn also die Ausführung des JavaScripts in Ihrem requestAnimationFrame-Callback länger als 16 ms dauert, können Sie keinen Frame rechtzeitig für die V-Sync erzeugen.
16 ms sind nicht viel Zeit. Glücklicherweise können Sie mit den Chrome-Entwicklertools herausfinden, ob Sie Ihr Frame-Budget während des requestAnimationFrame-Callbacks überschreiten.
Wenn wir die Zeitachse der Entwicklertools öffnen und eine Aufzeichnung dieser Animation in Aktion erstellen, sehen wir schnell, dass wir beim Animieren weit über dem Budget liegen. Wechseln Sie in der Zeitachse zu „Frames“ und sehen Sie sich Folgendes an:
Diese requestAnimationFrame-Callbacks (rAF) dauern länger als 200 ms. Das ist eine Größenordnung zu lang, um alle 16 ms einen Frame auszugeben. Wenn Sie einen dieser langen rAF-Callbacks öffnen, sehen Sie, was darin passiert: in diesem Fall viele Layouts.
In Pauls Video wird genauer auf die Ursache des Relayouts (scrollTop wird gelesen) und darauf eingegangen, wie es vermieden werden kann. Der Punkt ist, dass Sie sich den Callback genauer ansehen und herausfinden können, was so lange dauert.
Beachten Sie die Frame-Zeiten von 16 ms. Der leere Bereich in den Frames ist der Spielraum, den Sie haben, um mehr Arbeit zu erledigen (oder den Browser die Arbeit im Hintergrund erledigen zu lassen). Dieser Leerraum ist gut.
Andere Ursache für Ruckler
Die größte Ursache für Probleme beim Ausführen von JavaScript-basierten Animationen ist, dass andere Dinge den rAF-Callback behindern und sogar verhindern können, dass er überhaupt ausgeführt wird. Selbst wenn Ihr rAF-Callback schlank ist und nur wenige Millisekunden dauert, können andere Aktivitäten (z. B. die Verarbeitung eines gerade eingegangenen XHR, die Ausführung von Eingabe-Event-Handlern oder die Ausführung geplanter Aktualisierungen über einen Timer) plötzlich auftreten und für eine beliebige Zeitspanne ausgeführt werden, ohne dass sie unterbrochen werden. Auf Mobilgeräten kann die Verarbeitung dieser Ereignisse manchmal Hunderte von Millisekunden dauern. Während dieser Zeit wird die Animation vollständig angehalten. Diese Animationsfehler werden als Jank bezeichnet.
Es gibt kein Patentrezept, um diese Situationen zu vermeiden, aber es gibt einige Best Practices für die Architektur, die Ihnen helfen können:
- Führen Sie in Eingabe-Handlern nicht zu viele Verarbeitungsschritte aus. Viel JS oder der Versuch, die gesamte Seite z.B. während eines Onscroll-Handlers neu anzuordnen, ist eine sehr häufige Ursache für schreckliche Ruckler.
- Verschieben Sie so viel Verarbeitung wie möglich (also alles, was lange dauert) in Ihren rAF-Callback oder in Web-Worker.
- Wenn Sie Arbeit in den rAF-Callback verschieben, versuchen Sie, sie in kleinere Teile aufzuteilen, sodass Sie pro Frame nur ein wenig verarbeiten, oder verzögern Sie sie, bis eine wichtige Animation beendet ist. 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 Eingabe-Handler verschieben, 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 Ihrer rAF-Callbacks zu vermeiden. Sie können jedoch CSS-Animationen verwenden, um sie ganz zu 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 ausgeführt wird.
Im obigen Abschnitt zu Rucklern gibt es eine implizite Aussage: Browser können nur eine Sache gleichzeitig tun. Das ist zwar nicht ganz richtig, aber eine gute Arbeitshypothese: Der Browser kann zu einem bestimmten Zeitpunkt JS ausführen, das Layout erstellen oder rendern, aber immer nur eine dieser Aufgaben gleichzeitig. Dies kann in der Zeitachsenansicht der Entwicklertools überprüft werden. Eine der Ausnahmen von dieser Regel sind CSS-Animationen in Chrome für Android (und bald auch in Chrome für Computer, aber noch nicht).
Wenn möglich, vereinfacht die Verwendung einer CSS-Animation Ihre Anwendung und sorgt dafür, dass Animationen auch während der Ausführung von JavaScript reibungslos funktionieren.
// 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 stattdessen mit CSS-Animationen steuern, tritt das Ruckeln nicht mehr auf.
(Denken Sie daran, dass CSS-Animationen zum Zeitpunkt der Erstellung dieses Artikels nur in Chrome für Android und nicht in Chrome für Computer ruckelfrei sind.)
/* 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 Animationen ist es wichtig, für jede Bildschirmaktualisierung Frames zu erstellen. Vsync-Animationen haben einen enormen positiven Einfluss auf die Bedienung einer App.
- Die beste Möglichkeit, in Chrome und anderen modernen Browsern eine Vsync-Animation zu erhalten, ist die Verwendung von CSS-Animationen. Wenn Sie mehr Flexibilität benötigen, als CSS-Animationen bieten, ist die beste Technik die requestAnimationFrame-basierte Animation.
- Damit rAF-Animationen reibungslos funktionieren, sollten Sie darauf achten, dass andere Event-Handler die Ausführung des rAF-Callbacks nicht behindern. Außerdem sollten rAF-Callbacks kurz sein (< 15 ms).
Schließlich gilt die vsync-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 werden wir uns mit der Scrollleistung unter Berücksichtigung dieser Konzepte befassen.
Viel Spaß beim Animieren!