Jank-Busting für bessere Rendering-Leistung

Tom Wiltzius
Tom Wiltzius

Einleitung

Sie möchten, dass Ihre Webanwendung bei Animationen, Übergängen und anderen kleinen UI-Effekten reaktionsschnell und flüssig wirkt. Wenn diese Effekte nicht ruckelfrei sind, kann dies den Unterschied zwischen einem „nativen“ und einem schwerfälligen, unausgereiften Effekt ausmachen.

Dieser Artikel ist der erste in einer Reihe von Artikeln, in denen es um die Optimierung der Rendering-Leistung im Browser geht. Zunächst erklären wir, warum eine flüssige Animation schwierig ist und wie sie funktioniert. Außerdem werden einige Best Practices erläutert. Viele dieser Ideen wurden ursprünglich in „Jank Busters“ präsentiert, einem Vortrag von Nat Duca und mir bei einem diesjährigen Video zur Google I/O.

Jetzt neu: V-Sync

PC-Gamer kennen diesen Begriff vielleicht, aber im Web ist er 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. Mit V-Sync (oder vertikalen Synchronisierung) werden neue Frames nur zwischen Bildschirmaktualisierungen generiert. Sie können sich dies wie eine Wettlaufbedingung zwischen dem Prozess, der Daten in den Bildschirmpuffer schreibt, und dem Betriebssystem, das diese Daten liest, um sie auf dem Bildschirm bereitstellen, vorstellen. Der Inhalt des zwischengespeicherten Frames soll sich zwischen diesen Aktualisierungen nicht ändern, nicht während der Aktualisierung. 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 benötigen Sie bei jeder Bildschirmaktualisierung einen neuen Frame. Dies hat zwei große Auswirkungen: das Frame-Timing (d. h., wann der Frame bereit sein muss) und das Frame-Budget (d. h. wie lange der Browser einen Frame produzieren muss). Sie haben nur die Zeit zwischen den Bildschirmaktualisierungen, um einen Frame fertigzustellen (ca. 16 ms auf einem 60-Hz-Bildschirm), und Sie möchten mit der Produktion des nächsten Frames beginnen, sobald der letzte Frame auf dem Bildschirm angezeigt wurde.

Timing is Everything: requestAnimationFrame

Viele Webentwickler verwenden setInterval oder setTimeout alle 16 Millisekunden, um Animationen zu erstellen. Dies ist aus verschiedenen Gründen ein Problem, zu dem wir gleich noch mehr sprechen. Besonders besorgniserregend sind:

  • Die Timer-Auflösung von JavaScript erfolgt nur in der Reihenfolge mehrerer Millisekunden
  • Unterschiedliche Geräte haben unterschiedliche Aktualisierungsraten

Denken Sie an das oben erwähnte Frame-Timing-Problem: Sie benötigen einen fertigen Animations-Frame, der mit JavaScript, DOM-Manipulation, Layout, Malerei usw. abgeschlossen ist, um vor der nächsten Bildschirmaktualisierung bereit zu sein. Eine niedrige Timer-Auflösung kann es erschweren, Animationsframes vor der nächsten Bildschirmaktualisierung fertigzustellen, aber je nach Bildschirmaktualisierungsrate ist es mit einem festen Timer unmöglich. Unabhängig vom Timerintervall werden Sie für einen Frame langsam aus dem Timing-Fenster herausspringen und am Ende einen Frame verlieren. Das passiert auch dann, wenn der Timer mit einer Millisekunde genau ausgelöst wird, was dies nicht (wie Entwickler festgestellt haben) der Fall ist. Die Timer-Auflösung hängt davon ab, ob das Gerät im Akku- oder angeschlossenen Zustand ist. Dies kann z. B. durch Tabs beeinflusst werden, die im Hintergrund viele Ressourcen blockieren. Selbst wenn dies selten ist (z. B. alle 16 Frames, weil Sie um eine Millisekunde verschoben waren), werden Sie feststellen, dass Sie eine Sekunde verlieren. Außerdem generieren Sie Frames, die nie angezeigt werden, wodurch Energie und CPU-Zeit verschwendet werden, die Sie für andere Dinge in Ihrer Anwendung aufwenden könnten.

Verschiedene Displays haben unterschiedliche Aktualisierungsraten: 60 Hz ist üblich, aber einige Smartphones haben 59 Hz, manche Laptops fallen im Energiesparmodus auf 50 Hz und einige Desktop-Monitore auf 70 Hz.

Wir konzentrieren uns bei der Rendering-Leistung tendenziell auf die Bilder pro Sekunde (fps), aber Abweichungen können ein noch größeres Problem darstellen. Unsere Augen wahrnehmen die winzigen, unregelmäßigen Einflüsse in der Animation, die bei schlecht zeitgesteuerten Animationen entstehen können.

Mit requestAnimationFrame erhalten Sie korrekt zeitlich abgestimmte Animationsframes. Wenn Sie diese API verwenden, fordern Sie im Browser einen Animationsframe an. Ihr Callback wird aufgerufen, wenn der Browser bald einen neuen Frame erzeugen wird. Dies geschieht unabhängig von der Aktualisierungsrate.

requestAnimationFrame hat auch andere schöne Eigenschaften:

  • Animationen in Tabs im Hintergrund werden pausiert, wodurch Systemressourcen geschont werden und der Akku geschont wird.
  • Wenn das System das Rendering mit der Aktualisierungsrate des Bildschirms nicht verarbeiten kann, kann es Animationen drosseln und den Callback weniger häufig erzeugen (z. B. 30-mal pro Sekunde auf einem 60-Hz-Bildschirm). Dadurch wird die Framerate zwar halbiert, aber die Animation bleibt einheitlich. Wie bereits erwähnt, sind unsere Augen viel stärker auf Varianzen eingestellt als auf die Framerate. Ein gleichmäßiger 30-Hz-Wert sieht besser aus als 60 Hz, bei dem einige Bilder pro Sekunde fehlen.

requestAnimationFrame wurde bereits überall erwähnt. Lesen Sie daher Artikel wie diesen Artikel aus Creative JS, um mehr darüber zu erfahren, aber es ist ein wichtiger erster Schritt für eine reibungslose Animation.

Frame-Budget

Da bei jeder Bildschirmaktualisierung ein neuer Frame bereitsteht, bleibt nur die Zeit zwischen den einzelnen Aktualisierungen, um alles zu tun, um einen neuen Frame zu erstellen. Auf einem 60-Hz-Display haben wir dann etwa 16 ms Zeit, um den gesamten JavaScript-Code auszuführen, das Layout auszuführen, die Darstellung zu erstellen und alles andere, was der Browser tun muss, um den Frame herauszuholen. Wenn also die Ausführung von JavaScript in Ihrem requestAnimationFrame-Callback länger als 16 ms dauert, besteht keine Hoffnung, rechtzeitig einen Frame für die V-Sync-Datei zu erzeugen.

16 ms sind nicht viel Zeit. Die Entwicklertools von Chrome können Ihnen dabei helfen, herauszufinden, ob Ihr Frame-Budget während des requestAnimationFrame-Callbacks überschritten wird.

Wenn wir die Zeitachse der Entwicklertools öffnen und eine Aufzeichnung dieser Animation in Aktion machen, zeigt sie schnell, dass wir bei der Animation weit über dem Budget liegen. Wechsle auf der Zeitachse zu „Frames“ und sieh dir Folgendes an:

Eine Demo mit viel zu viel Layout
Eine Demo mit viel zu viel Layout

Diese rAF-Callbacks (requestAnimationFrame) benötigen > 200 ms. Diese Größenordnung ist zu lang, um alle 16 ms einen Frame zu durchstreichen. Das Öffnen eines dieser langen rAF-Callbacks zeigt, was drinnen vor sich geht: in diesem Fall viel Layout.

In Pauls Video geht es um die genaue Ursache der Neuanordnung (siehe scrollTop) und wie sie vermieden werden kann. Aber hier ist der Sinn, dass Sie in den Callback gehen und herausfinden können, was so lange dauert.

Eine aktualisierte Demo mit viel reduziertem Layout
Eine aktualisierte Demo mit stark reduziertem Layout

Beachten Sie die Framezeiten von 16 ms. Diese freie Fläche in den Frames ist der Spielraum, den Sie für mehr Arbeit benötigen (oder dass der Browser die Arbeit im Hintergrund erledigen kann). Diese freie Fläche ist eine gute Sache.

Andere Quelle von Jank

Die größte Ursache für Probleme bei der Ausführung von JavaScript-gestützten Animationen ist, dass andere Dinge den rAF-Callback aufhalten und sogar daran hindern können, ihn auszuführen. Selbst wenn Ihr rAF-Callback schlank ist und in wenigen Millisekunden ausgeführt wird, können andere Aktivitäten wie die Verarbeitung einer gerade eingegangenen XHR, das Ausführen von Eingabe-Event-Handlern oder das Ausführen geplanter Updates für einen Timer plötzlich eintreten und für einen beliebigen Zeitraum ohne Ergebnisse ausgeführt werden. Auf Mobilgeräten kann die Verarbeitung dieser Ereignisse manchmal Hunderte von Millisekunden dauern. In diesem Zeitraum bleibt die Animation vollständig angehalten. Diese Animationen werden als Jank bezeichnet.

Es gibt keine Zauberkugel, diese Situationen zu vermeiden, aber es gibt ein paar Best Practices für die Architektur, mit denen Sie die Weichen auf Erfolg stellen können:

  • Führen Sie nicht zu viele Verarbeitungsvorgänge in Eingabe-Handlern durch. Viel JS-Code oder der Versuch, die gesamte Seite neu zu ordnen, beispielsweise mit einem Onscroll-Handler, ist eine häufige Ursache für schlimme Verzögerungen.
  • Übertragen Sie möglichst viele Verarbeitungsvorgänge (also alles, dessen Ausführung lange dauert), Ihrem rAF-Callback oder Ihren Web Workers.
  • Wenn Sie Arbeit in den rAF-Callback stecken, versuchen Sie, ihn so aufzuteilen, dass Sie nur ein wenig jeden Frame verarbeiten, oder verzögern Sie ihn, bis eine wichtige Animation beendet ist. So können Sie weiterhin kurze rAF-Callbacks ausführen und reibungslos animieren.

Ein gutes Tutorial, in dem erläutert wird, wie Sie die Verarbeitung in requestAnimationFrame-Callbacks statt in Eingabe-Handler umwandeln können, finden Sie im Artikel Leaner, Meaner, Faster Animations with requestAnimationFrame von Paul Lewis.

CSS-Animation

Was ist besser als vereinfachtes JS in deinen Event- und rAF-Callbacks? Kein JS.

Zuvor haben wir gesagt, dass es keine Patentlösung dafür gibt, die Unterbrechung Ihrer rAF-Callbacks zu vermeiden. Sie können jedoch CSS-Animationen verwenden, um diese vollständig zu vermeiden. Insbesondere in Chrome für Android (und in anderen Browsern, in denen ähnliche Funktionen verwendet werden) haben CSS-Animationen die wünschenswerte Eigenschaft, dass sie im Browser häufig auch dann ausgeführt werden können, wenn JavaScript ausgeführt wird.

Im obigen Abschnitt gibt es eine implizite Anweisung zur Verzögerung: Browser können jeweils nur eine Sache tun. Dies ist nicht ganz richtig, aber es ist eine gute Annahme: Der Browser kann jederzeit JS ausführen, Layout ausführen oder malen, aber nur jeweils einer. Das lässt sich in der Zeitachsenansicht der Entwicklertools prüfen. Eine der Ausnahmen sind CSS-Animationen in Chrome für Android (und bald auch in der Desktopversion von Chrome).

Wenn möglich, vereinfacht die Verwendung einer CSS-Animation Ihre Anwendung und sorgt dafür, dass Animationen reibungslos ausgeführt werden, selbst wenn JavaScript ausgeführt wird.

  // 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 ausgeführt, was zu einer Verzögerung führt. Wenn wir diese Animation jedoch mit CSS-Animationen steuern, tritt die Verzögerung nicht mehr auf.

Zur Erinnerung: Zum Zeitpunkt der Abfassung dieses Artikels ist die CSS-Animation nur in Chrome für Android und nicht in der Desktopversion von Chrome ohne Verzögerungen möglich.

  /* 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 Animations finden Sie in diesem Artikel über MDN.

Zusammenfassung

Kurz gesagt:

  1. Bei Animationen ist es wichtig, für jede Bildschirmaktualisierung Frames zu erzeugen. VSync-Animationen wirken sich positiv auf das Erscheinungsbild einer App aus.
  2. Die vsync-Animation in Chrome und anderen modernen Browsern kann am besten mit CSS-Animationen wiedergegeben werden. Wenn Sie mehr Flexibilität benötigen, als die CSS-Animation bietet, ist die auf „requestAnimationFrame“ basierende Animation das beste Verfahren.
  3. Damit die rAF-Animationen fehlerfrei und fehlerfrei sind, sollten Sie darauf achten, dass Ihr rAF-Callback nicht durch andere Event-Handler im Weg ist. Außerdem sollten die rAF-Callbacks kurz sein (< 15 ms).

VSync-Animationen gelten nicht nur für einfache UI-Animationen, sondern auch für Canvas2D-Animationen, WebGL-Animationen und sogar das Scrollen auf statischen Seiten. Im nächsten Artikel der Reihe befassen wir uns mit der Scrollleistung unter Berücksichtigung dieser Konzepte.

Viel Spaß beim Animieren!

Verweise