Jank-Busting für bessere Rendering-Leistung

Tom Wiltzius
Tom Wiltzius

Einführung

Sie möchten, dass sich Ihre Webanwendung beim Ausführen von Animationen, Übergängen und anderen kleinen UI-Effekten responsiv und flüssig anfühlt. Wenn bei diesen Effekten keine Verzögerungen auftreten, kann dies den Unterschied zwischen einer schwerfällig oder unausgereift sind.

Dies ist der erste einer Reihe von Artikeln zur Optimierung der Rendering-Leistung im Browser. Zu Beginn erfahren Sie, warum flüssige Animationen schwierig sind und was erforderlich ist, um dieses Ziel zu erreichen, sowie einige einfache Best Practices. Viele dieser Ideen wurden ursprünglich in "Jank Busters" ein Vortrag von Nat Duca und mir auf der Google I/O dieses Jahr (Video).

Jetzt neu: V-Sync

PC-Gamer sind mit diesem Begriff möglicherweise vertraut, im Web ist er jedoch unüblich: Was ist v-sync?

Denken Sie an das Display Ihres Telefons: Es wird in regelmäßigen Abständen aktualisiert, normalerweise (aber nicht immer!) etwa 60-mal pro Sekunde. V-Sync (oder vertikale Synchronisierung) bezieht sich auf die Erzeugung neuer Frames nur zwischen Bildschirmaktualisierungen. Sie können sich dies als eine Wettlaufbedingung zwischen dem Prozess, der Daten in den Bildschirmzwischenspeicher schreibt, und dem Betriebssystem, das diese Daten liest, vorstellen, um sie auf dem Bildschirm zu speichern. Wir möchten, dass sich der Inhalt des gepufferten Frames zwischen diesen Aktualisierungen ändert, nicht während der Aktualisierungen. Andernfalls werden auf dem Monitor die Hälfte eines Frames und die Hälfte des anderen angezeigt, was zu Reißen führt.

Für eine flüssige Animation muss bei jeder Bildschirmaktualisierung ein neuer Frame bereitgestellt werden. 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 zu beenden (ca. 16 ms bei einem Bildschirm mit 60 Hz) und Sie möchten mit der Produktion des nächsten Frames beginnen, sobald der letzte Frame auf dem Bildschirm eingeblendet 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, auf das wir später noch genauer eingehen werden, aber hier sind folgende Punkte besonders wichtig:

  • Die Timer-Auflösung durch JavaScript beträgt nur einige 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-Bearbeitungen, Layout, Painting usw. fertig ist, bevor die nächste Bildschirmaktualisierung durchgeführt wird. Eine niedrige Timer-Auflösung kann es schwierig machen, Animationsframes vor der nächsten Bildschirmaktualisierung fertigzustellen, aber Schwankungen der Bildschirmaktualisierungsrate machen es mit einem festen Timer unmöglich. Unabhängig vom Timerintervall driften Sie langsam aus dem Timing-Fenster für einen Frame heraus und lassen einen Frame weg. Das passiert auch dann, wenn der Timer millisekundengenau ausgelöst wird, was Entwicklern festgestellt haben würde. Die Timer-Auflösung hängt davon ab, ob der Computer im Akkubetrieb ist oder an eine Steckdose angeschlossen ist. Auch wenn dies selten ist (z. B. alle 16 Frames, weil Sie um eine Millisekunde weg waren), werden Sie feststellen, dass Sie eine Sekunde abgebrochen haben. Sie werden auch Frames generieren, 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 eine 59 Hz, manche Laptops im Energiesparmodus auf 50 Hz und einige Desktop-Monitore sind 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 sein. Unsere Augen bemerken die winzigen, unregelmäßigen Fehler in der Animation, die eine schlecht getaktete Animation erzeugen kann.

Um korrekt getaktete Animationsframes zu erhalten, verwenden Sie requestAnimationFrame. Wenn Sie diese API verwenden, fordern Sie einen Animationsframe vom Browser an. Ihr Callback wird aufgerufen, wenn der Browser bald einen neuen Frame produziert. Dies geschieht unabhängig von der Aktualisierungsrate.

requestAnimationFrame hat noch weitere tolle Eigenschaften:

  • Animationen in Tabs im Hintergrund werden angehalten, um Systemressourcen zu sparen und die Akkulaufzeit zu verlängern.
  • Wenn das System das Rendering mit der Aktualisierungsrate des Bildschirms nicht verarbeiten kann, kann es Animationen drosseln und den Callback seltener auslösen, z. B. 30-mal pro Sekunde bei einem 60-Hz-Bildschirm. Dadurch wird zwar die Framerate halbiert, aber die Animation bleibt einheitlich. Wie bereits erwähnt, sind unsere Augen viel stärker auf Varianz ausgerichtet als auf die Framerate. Ein konstanter Wert von 30 Hz sieht besser aus als 60 Hz, bei dem ein paar Bilder pro Sekunde ausgelassen werden.

Über requestAnimationFrame wurde bereits überall diskutiert. Weitere Informationen dazu finden Sie in diesem Artikel von Creative JS, aber es ist ein wichtiger erster Schritt für eine reibungslose Animation.

Frame-Budget

Da bei jeder Bildschirmaktualisierung ein neuer Frame bereit sein soll, gibt es nur die Zeit zwischen den einzelnen Aktualisierungen, um einen neuen Frame zu erstellen. Bei einem 60-Hz-Display haben wir noch ca. 16 ms Zeit, um JavaScript auszuführen, Layout, Darstellung und andere Aktionen durchzuführen, die der Browser sonst noch ausführen muss, um den Frame herauszuholen. Wenn also die Ausführung des JavaScript-Codes in deinem requestAnimationFrame-Callback länger als 16 ms dauert, hast du nicht die Hoffnung, rechtzeitig einen Frame für v-sync zu erzeugen.

16 ms ist nicht viel Zeit. Glücklicherweise können Sie mit den Entwicklertools von Chrome herausfinden, ob Ihr Frame-Budget während des requestAnimationFrame-Callbacks angebrannt wird.

Wenn wir die Zeitachse der Entwicklertools öffnen und eine Aufzeichnung dieser Animation in Aktion machen, zeigen wir schnell, dass wir das Budget bei der Animation deutlich überschritten haben. Wechseln Sie auf der Zeitachse zu „Frames“. und sieh dir Folgendes an:

<ph type="x-smartling-placeholder">
</ph> Eine Demo mit viel zu viel Layout
Eine Demo mit viel zu viel Layout

Diese requestAnimationFrame-Callbacks (rAF) dauern mehr als 200 ms. Das ist eine Größenordnung zu lang, um alle 16 ms einen Frame abzuhaken! Wenn Sie einen dieser langen rAF-Callbacks öffnen, sehen Sie, was im Inneren vor sich geht: in diesem Fall viel Layout.

Pauls Video geht näher auf die spezifische Ursache der Layout-Anpassung (scrollTop) ein und erklärt, wie sie vermieden werden können. Wichtig ist hier jedoch, dass Sie in den Callback einsteigen und untersuchen können, was so lange dauert.

<ph type="x-smartling-placeholder">
</ph> Eine aktualisierte Demo mit stark reduziertem Layout
Aktualisierte Demo mit stark reduziertem Layout

Beachten Sie die Frame Time von 16 ms. Dieser leere Bereich in den Frames ist der Spielraum, den Sie für mehr Arbeit haben (oder den Browser im Hintergrund erledigen lassen). Dieser Leerraum ist eine gute Sache.

Andere Quelle von Jank

Die größte Ursache für Probleme bei der Ausführung von JavaScript-basierten Animationen dass der rAF-Rückruf durch andere Dinge beeinträchtigt werden kann zu verhindern. Auch wenn der rAF-Callback sehr kurz ist und nur wenige Millisekunden, andere Aktivitäten (wie die Verarbeitung einer gerade empfangenen XHR) das Ausführen von Eingabe-Event-Handlern oder das Ausführen geplanter Updates für einen Timer) können plötzlich und für einen beliebigen Zeitraum geschaltet werden, ohne nachzugeben. Auf Mobilgeräten kann die Verarbeitung dieser Ereignisse hunderte von Millisekunden dauern. in dem Ihre Animation vollständig angehalten wird. Diese nennen wir Animations-haken jank.

Es gibt keine Zauberei, um solche Situationen zu vermeiden. Es gibt jedoch einige Best Practices für die Architektur, mit denen Sie die Weichen auf Erfolg stellen können:

  • Nehmen Sie nicht viel Verarbeitung in Eingabe-Handlern vor. Durch viel JS-Code oder der Versuch, die gesamte Seite neu anzuordnen, z.B. ist ein Onscroll-Handler eine häufige Ursache für schreckliche Verzögerungen.
  • Übertragen Sie möglichst viele Verarbeitungsvorgänge (Lesezugriff: alles, bei dem die Ausführung sehr lange dauert) in Ihren rAF-Callback oder in die Web Worker.
  • Wenn Sie Arbeit in den rAF-Callback stecken, versuchen Sie, ihn so aufzuteilen, dass Sie jeden Frame nur ein wenig verarbeiten, oder warten Sie, bis eine wichtige Animation beendet ist. Auf diese Weise können Sie weiterhin kurze rAF-Callbacks ausführen und reibungslos animieren.

Ein tolles Tutorial, in dem gezeigt wird, wie man die Verarbeitung in requestAnimationFrame-Rückrufe anstelle von Eingabe-Handlern übt, findet ihr im Artikel Leaner, Meaner, Faster Animations with requestAnimationFrame.

CSS-Animation

Was ist besser als einfaches JS in deinen Ereignis- und rAF-Callbacks? Kein JS.

Wir haben bereits erwähnt, dass es keine Patentlösung gibt, um die Unterbrechung von rAF-Callbacks zu vermeiden, aber Sie können CSS-Animationen verwenden, um die Notwendigkeit ganz zu vermeiden. Insbesondere bei Chrome für Android (und in anderen Browsern arbeiten an ähnlichen Funktionen), haben CSS-Animationen die gewünschte Eigenschaft, dass der Browser sie oft selbst dann ausführen kann, wenn JavaScript ausgeführt wird.

Im obigen Abschnitt finden Sie eine implizite Aussage zu Verzögerungen: Browser können jeweils nur eine Aktion ausführen. Dies ist nicht ganz wahr, aber eine gute Arbeitsannahme ist: Der Browser kann zu jeder Zeit JavaScript ausführen, Layout oder Painting ausführen, jedoch immer nur jeweils eins. Dies kann in der Zeitachsenansicht der Entwicklertools überprüft werden. Eine Ausnahme von dieser Regel sind CSS-Animationen in Chrome für Android (und bald auch in Chrome für Computer, jedoch noch nicht).

Wenn möglich, vereinfacht die Verwendung von CSS-Animationen Ihre Anwendung und sorgt dafür, dass Animationen auch während der Ausführung von JavaScript reibungslos 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 Verzögerungen führt. Wenn wir diese Animation jedoch mit CSS-Animationen steuern, tritt die Verzögerung nicht mehr auf.

Zum Zeitpunkt der Veröffentlichung dieses Dokuments funktionieren CSS-Animationen nur bei Chrome für Android ohne Verzögerungen, nicht in Chrome auf dem Desktop.

  /* 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 diesem Artikel auf MDN.

Zusammenfassung

Das Kürzel lautet:

  1. Bei Animationen ist es wichtig, Frames für jede Bildschirmaktualisierung zu erstellen. Vsync-Animationen haben großen positiven Einfluss auf das Gefühl einer App.
  2. Die beste Möglichkeit, vsync-Animationen in Chrome und anderen modernen Browsern zu erhalten, um CSS-Animationen zu verwenden. Wenn Sie mehr Flexibilität als CSS-Animationen benötigen ist die beste Methode die requestAnimationFrame-basierte Animation.
  3. Achten Sie darauf, dass andere Event-Handler die Ausführung des rAF-Callbacks nicht behindern, und die rAF-Callbacks beibehalten. kurz (< 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 dieser Reihe befassen wir uns mit der Scrollleistung unter Berücksichtigung dieser Konzepte.

Viel Spaß beim Animieren!

Verweise