, ,

Perfomante Animationen

Niklas Brocker

Animationen sind sehr ansprechend und bringen User dazu, über eine längere Zeit auf einer Webseite zu bleiben oder von dort aus nach weiteren Informationen des Anbieters zu suchen. Aus diesem Grund ist es wichtig, dass sie ihre Funktion erfüllen und flüssig laufen. Trotzdem stößt man immer wieder auf Animationen, die ruckeln und nicht gut performen. Insbesondere bei mobilen Endgeräten mit ihrer vergleichsweise geringen Rechenleistung fallen solche Probleme auf – und damit ausgerechnet in einem Bereich, der immer bedeutsamer wird. Laut eines Google-Berichtes stehen bereits hinter mehr als fünfzig Prozent des Datenverkehrs mobile Endgeräte. Animationen sollten daher auf jeden Fall optimiert werden.

Um die perfekte Animation zu erstellen, gibt es nicht die eine Lösung. Doch dieser Artikel versucht, einige Punkte zu vermitteln, die man als Entwickler beachten sollte. Grundlage dafür ist das Verständnis von CSS, JavaScript und HTML.

Was ist eine Animation? Mit einer Animation kann die Änderung von CSS Properties dargestellt werden. Wenn sich eine solche Property ändert, zeigt der Browser diese bei einer Animation nicht abrupt, sondern als Übergang an. Wenn ein Element eine volle Deckkraft hat und entfernt wird, würde es normalerweise verschwinden. Mit einem Übergang verliert das Element über Zeit die Deckkraft, bis es komplett verschwunden ist. Entwickler können den Verlauf (z.B. lineare Änderung der CSS Property) und die Dauer der Übergänge anpassen.

Animations oder Transitions oder JavaScript?

Grundsätzlich gibt es verschiedene Möglichkeiten, Übergänge zu erstellen. Auf drei davon, soll an dieser Stelle eingegangen werden: Transitions, Animations bzw. Keyframes und JavaScript.

Transitions sind die einfachere Variante von Übergängen. Entwickler können mit ihnen den Wechsel von Zustand A zu B definieren. Am häufigsten sind Transitions zu sehen, wenn zwischen solchen Zuständen gewechselt werden muss. Beispiele sind Effekte beim Hovern eines Elements oder das Öffnen eines Side-Menüs. Das Definieren einer Transition funktioniert folgendermaßen:

.btn {
  opacity: 1;
  transition: opacity 0.5s;
}

.btn:hover {
  opacity: 0.5;
}

In diesem Beispiel lässt sich ein Button mit voller Deckkraft erkennen. In der zweiten Zeile der Definition des Buttons ist die Transition festgehalten. Immer wenn sich die Deckkraft des Elements ändert, wird dies über einen Zeitraum von 0,5s gemacht. Wenn der Button gehovert wird, ändert sich die opacity auf 0,8. Dementsprechend kommt es zu einer Animation des Übergangs.

Keyframes hingegen sind mächtiger, aber auch komplizierter. Sie ermöglichen es dem Entwickler, mehr Zustände zu definieren. So kann eine Animation den Wechsel von A –> B –> C –> D darstellen. Durch ihre Komplexität ist die Definition auch aufwendiger:

@keyframes foo {
  0% { opacity: 1; }
  50% { opacity: 0.5; }
  100% { opacity: 1; }
}

.btn {
  opacity: 1;
  animation: foo 0.5s;
}

In dem @keyframes werden die einzelnen Zustände definiert. Dieses Keyframe wird dann über das CSS-Attribut animation dem Element zugewiesen.

JavaScript Animationen sind eine weitere Möglichkeit, Übergänge zu erstellen. Dazu muss der Entwickler die Styles in kurzer Zeit oft anpassen. Das folgende Beispiel stammt aus w3schools (https://www.w3schools.com/howto/howto_js_animate.asp):

function myMove() {
  var elem = document.getElementById("myAnimation");
  var pos = 0;
  var id = setInterval(frame, 10);
  function frame() {
    if (pos == 350) {
      clearInterval(id);
    } else {
      pos++;
      elem.style.top = pos + 'px';
      elem.style.left = pos + 'px';
    }
  }
}

Hier wird das Styling des Elements alle 10ms angepasst. Dafür verwendet die Funktion die setInterval-Methode. Das führt dazu, dass das Element „animiert“ wird. Sobald das Element 350-mal bewegt wurde, endet die Animation bzw. das Intervall.

In der JavaScript-Welt gibt es noch mehr Wege, Übergänge umzusetzen. Viele Libraries (wie z.B. JQuery) haben Schnittstellen, mit denen Animationen erstellt und ausgelöst werden können. Zusätzlich gibt es die Web Animations API, die es ermöglicht, Keyframes Animationen über JavaScript zu erstellen. Eine detaillierte Beschreibung der Web Animations API lässt sich hier finden: https://css-tricks.com/css-animations-vs-web-animations-api/.

  • Alle Varianten haben ihre Vorzüge und eignen sich für jeweils verschiedene Anwendungen. Man sollte Transitions verwenden, wenn man zwischen zwei Zuständen hin und her wechseln will und dafür einen Übergang braucht.
  • Man sollte Animations verwenden, wenn man mehr als zwei Zustände hat und dafür Übergänge braucht. Animations erlauben auch Übergänge, die unendlich laufen.
  • Man sollte JavaScript verwenden, wenn man die volle Kontrolle über die Animation braucht. Das ist der Fall bei Effekten wie Pausieren oder Stoppen. Die Umsetzung der Übergänge kann hierbei selbst implementiert werden (s.o.) oder Entwickler können die Schnittstellen der Web Animations API bzw. von Libraries verwenden.

Der Einfachheit halber werden im Folgenden alle Varianten der Übergänge als Animationen bezeichnet.

Was macht eine gute Performance aus?

Das Ziel bei der Umsetzung aller genannten Varianten ist eine flüssige und performante Animation. Um dies zu erreichen, müssen 60FPS erreicht werden. FPS steht für „frames per second“. Pro Sekunde soll also das gewählte Element 60-mal angepasst werden. Denn: wenn Bilder sehr oft pro Sekunde geändert werden, sieht es für den Menschen wie eine flüssige Bewegung aus. Umgerechnet bedeutet das: der Browser hat 16,67ms, um ein Frame zu berechnen. Das ist nicht viel Zeit. Das muss dem Entwickler klar sein. Besonders wichtig ist dies, wenn die Nutzer auf Endgeräten sind, die eine schlechtere Rechenleistung besitzen. Dazu zählen zum Beispiel mobile Endgeräte. Wenn die genannten 60FPS nicht möglich sind bzw. die Berechnung eines Frames länger als 16,67ms dauert, nennt man das Jank. Es werden Frames übersprungen. Da sich die Bilder nicht mehr in der gewünschten Rate ändern, kann der Mensch dies wahrnehmen. Die Folge ist, dass die Animation ruckelt. In Zukunft ist es wahrscheinlich, dass die erwünschten FPS noch weiter ansteigen, da die Rechner immer mehr Rechenkapazitäten bekommen. Dies wird eine noch geringere Zeit zur Berechnung eines Frames zur Folge haben. Eine Optimierung von Animationen wird also in der Zukunft noch wichtiger sein.

Die Optimierung einer Animation: Was passiert im Browser?

Bevor wir uns an die Optimierung einiger Beispiele setzen, will ich noch erklären, wie der Browser Inhalte anzeigt. Dadurch soll klarer werden, wie wir unser Ziel – 60FPS – erreichen. Was macht der Browser also? Hierbei gibt es verschiedene Phasen, die er durchläuft:

  • Styling: In dieser Phase berechnet der Browser das Styling der einzelnen Elemente anhand der CSS-Klassen, der IDs usw. Der Browser bestimmt welche Änderungen er am aktuellen Inhalt vornehmen muss, um diese umzusetzen. Nach dieser Phase folgt entweder Layout, Paint oder Composite.
  • Layout: Der Browser berechnet an welche Position die einzelnen Elemente stehen. Folgende CSS-Attribute sind hier unter anderem relevant:
    • Margin
    • Padding
    • Width und Height
    • Left, Top, Bottom, Right
  • Paint: Nun muss der Browser die einzelnen Bereiche „bemalen“. Er befüllt also die Pixel. Das betrifft unter anderem folgende CSS-Attribute:
    • Color
    • Background-color
    • Visibility
  • Composite: Zuletzt geht es an die Anordnung. Dabei werden die einzelnen Bereiche zusammengebaut. Falls dabei mehrere Ebenen notwendig sind (z.B. absolute Elemente), ordnet der Browser diese korrekt an. Er achtet dabei unter anderem auf folgende CSS-Attribute:
    • Transform
    • Opacity
    • Z-Index
    • Pointer

Hierbei ist wichtig zu verstehen, dass Layout –> Paint –> Composite in ihrer Reihenfolge festgelegt sind. Das heißt: Wenn der Browser ein Layout durchführen muss, dann müssen auch ein Paint und ein Composite folgen. Wenn hingegen nur ein Composite stattfindet, ist kein Layout notwendig.

Wenn JavaScript auf einer Seite nun z.B. die Breite eines Elements ändert, hat dies zur Folge, dass die Schritte Layout –> Paint –> Composite notwendig sind. Eine einmalige Änderung im Layout ist dabei kein Problem. Wenn man dies allerdings 60-mal in der Sekunde versucht, kann es zu Jank kommen. Viel besser ist es also, im Bereich Composite zu bleiben, da der Browser weniger Rechenschritte durchführen muss.

Die Optimierung einer Animation: Side-Menü mit Transitions

Wie sich eine Animation optimieren lässt, lässt sich am besten Anhand eines Beispiels zeigen: ein Side-Menü. Ausgangspunkt soll dabei zunächst der folgende Code sein:

HTML

<body>
<div id=“menu“><button onclick=”toggleMenu()”>Schließen</button><div>
<main>
   <h1>Mein Inhalt</h1>
   <div class=”btn-wrapper”>
      <button onclick=”toggleMenu()”>Menü öffnen</button>
   <div>
</main>
</body>

JavaScript

function toggleMenu() {
  var menu = document.getElementById(“menu”);
  menu.classList.toggle(“menu-open”);
}

CSS

#menu {
    height: 100%;
    width: 260px;
    position: absolute;
    margin-left: -310px;
    background-color: indianred;
    display: flex;
    padding: 20px;
    flex-direction: column;
    align-items: flex-end;
    transition: margin-left 1s;
}

.open-menu {
    margin-left: 0 !important;
}

button {
    transition: background-color .3s;
    padding: 10px;
    background-color: #2c3e50;
    color: white;
    cursor: pointer;
    border: none;
}

button:hover {
    background-color: #44596e;
}

Wenn der Nutzer auf einen der Buttons klickt, wird das Menü entweder geöffnet oder geschlossen. Das Menü ist normalerweise nicht sichtbar, da es mit margin-left aus dem Bild geschoben ist. Wenn das Menü die Klasse „open-menu“ erhält, setzt man margin-left auf 0. Dadurch ist das Menü sichtbar. Neben dem Menü bekommen die Buttons noch etwas Styling und einen Hover-Effekt. Die Animationen sind hier also die Bewegung des Menüs und das Ändern der Hintergrundfarbe. Erkennbar ist dies im CSS durch das Attribut transition.

Ein Ansatzpunkt zur Optimierung der Animation wäre in diesem Fall das Öffnen des Menüs. Hier sind folgende CSS-Styles relevant:

#menu {
    /* restliche Styles... */
    transition: margin-left 1s;
}

.open-menu {
    margin-left: 0 !important;
}

In diesem Beispiel müsste der Browser also immer wieder Layout, Paint und Composite durchlaufen, denn margin-left gehört zum Layout-Schritt. Besser wäre Folgendes:

#menu {
    /* restliche Styles... */
    transition: transform 1s;
}

.open-menu {
    transform: translateX(300px);
}

Mit dieser Animation müsste der Browser nur das Composite anpassen.

Neben dieser Maßnahme gibt es noch weiter Möglichkeiten, um Animationen zu optimieren. Man kann dem Browser mit folgendem CSS Attribut mitteilen, dass sich bestimmte Attribute ändern werden:

will-change: transform;

Der Browser stellt sich dann auf die Änderungen ein und optimiert diese, indem er mehr Ressourcen als sonst in die Animation steckt. Allerdings kann sich will-change auch negativ auf die Performance auswirken. Wenn eine Seite das Attribut zu häufig verwendet, steckt der Browser insgesamt zu viele Ressourcen in die genannten Animationen. Dadurch kann die Performance der restlichen Seite leiden. Das Attribut sollte also nur als letztes Mittel genutzt werden. Falls man dem Menü noch mehr Ressourcen zuweisen möchte, ändern sich die Styles folgendermaßen:

#menu {
   /* restliche Styles... */
   will-change: transform;
}

Generell gilt: Weniger ist mehr. Wenn der Browser viele Animationen gleichzeitig berechnen muss oder mit anderen Dingen beschäftigt ist, ist die Wahrscheinlichkeit höher, dass es zu Jank kommt. Aus diesem Grund sollte die Seite auch keine Animationen anzeigen, wenn die Seite noch nicht komplett geladen ist. Das Binden von Animationen an bestimmte Events, etwa an das Srolling, ist aus Sicht der Performance ebenfalls kritisch, denn der Browser sendet beim Scrolling viele Events. Man muss bei entsprechenden Animationen sehr vorsichtig sein.

Die Optimierung einer Animation: Side-Menü mit JavaScript

Ein zweites Beispiel soll zeigen, wie sich JavaScript Animationen optimieren lassen. Um das Menü zu öffnen, nutzt man hier nicht CSS, sondern JavaScript. Als Orientierung für die Funktion dient an dieser Stelle das obige Beispiel. Das Styling des Menüs bleibt quasi gleich. Die Funktion zum Öffnen/ Schließen des Menüs sieht so aus:

function jsMenuToggle(open) {
  var menu = document.getElementById('menu');

  var id = setInterval(function () {
    var marginLeft = menu.style.marginLeft || '-310px';
    var marginLeftInt = parseInt(marginLeft.replace('px', ''));

    if (marginLeftInt === 0 && open || marginLeftInt === -310 && !open) {
      clearInterval(id);
    } else if (!open) {
      menu.style.marginLeft = (marginLeftInt - 1) + 'px';
    } else {
      menu.style.marginLeft = (marginLeftInt + 1) + 'px';
    }
  }, 10);
}

Es passiert folgendes:

  • Open teilt der Funktion mit, ob sie das Menü öffnen oder schließen soll.
  • Menu enthält die Referenz zum Side-Menü
  • Anschließend erstellt man ein Intervall. Die mitgegebene Funktion wird alle 10ms aufgerufen.
  • In den nächsten beiden Zeilen wird der aktuelle Wert von margin-left extrahiert.
  • Am Ende kontrolliert man, ob das Menü seine Endzustände erreicht hat. Wenn ja, wird das Intervall beendet. Wenn nein, ändert sich der der Wert von margin-left.

In diesem Beispiel erfüllen Intervalle grundsätzlich ihren Zweck. Der Code manipuliert immer wieder in kurzen Zeitabständen die Styles des Elements. Allerdings führen die Intervalle zu einigen Problemen:

  • Sie laufen auch im Hintergrund weiter. Das führt zu einem höheren Batterieverbrauch beim Client.
  • Verschiedene Geräte haben unterschiedliche Framerates. Somit kann der Code entweder zu wenige oder zu viele Aufrufe generieren. 10ms könnten je nach Gerät stimmen – oder auch nicht.
  • Der Code ist nicht mit dem Rendering des Browsers synchronisiert. Wenn der Browser bereits angefangen hat, einen Frame zu berechnen, und anschließend dieser Code ausgelöst wird, bleibt für seine Ausführung unter Umständen noch weniger Zeit als 16ms. Die Berechnung dauert zu lange und es entsteht Jank.
  • In diesem Intervall wird das Styling von mergin-left manipuliert. Wie wir bereits erläutert haben, ist dies nicht optimal.

Die Lösung für die genannten Probleme liefert die Funktion: requestAnimationFrame. Sie teilt dem Browser mit, dass man eine Funktion ausführen will, wenn der nächste Frame berechnet wird. Die Funktion passt sich an die Framerates der einzelnen Geräte an und spart dadurch Akkuladung. Die optimierte Funktion sieht dann folgendermaßen aus:

function jsMenuToggle(open) {
  var menu = document.getElementById('menu');

  var frame = function () {
    var transform = menu.style.transform || 'translateX(0px)';
    var transformInt = parseInt(transform.replace('translateX(', '').replace('px)', ''));

    if (transformInt === 300 && open || transformInt === 0 && !open) {
      return;
    } else if (!open) {
      menu.style.transform = 'translateX(' + (transformInt - 1) + 'px)';
    } else {
      menu.style.transform = 'translateX(' + (transformInt + 1) + 'px)';
    }
    window.requestAnimationFrame(frame);
  }
  window.requestAnimationFrame(frame);
}

Im Zuge der Optimierung haben sich folgende Dinge geändert:

  • Man definiert kein Intervall mehr, sondern eine einfache innere Funktion frame.
  • Diese Funktion ändert nun transform und nicht mehr margin-left.
  • Falls das Menü noch nicht komplett geöffnet oder geschlossen ist, ruft sie am Ende requestAnimationFrame auf. Als Parameter gibt die Funktion sich selber mit. Es ist also eine Art rekursiver Aufruf.
  • Am Ende der äußeren Funktion startet die Animation, indem die innere Funktion frame an requestAnimationFrame gegeben wird.

Der Browser kann nun die innere Funktion zum optimalen Zeitpunkt aufzurufen. Außerdem muss er nicht mehr das Layout berechnen.

Die Optimierung einer Animation: Die Buttons des Side-Menüs

Des Weiteren zu beachten sind die Buttons. Ihre Hintergrundfarbe wird animiert, wenn der Nutzer sie hovert. Und das führt zu einem Painting –> Composite. Daraus ergeben sich keine großen Probleme, aber optimal ist es nicht.

Doch wie lässt sich das Problem lösen – zumal weder transform noch opacity die Farbe des Elements ändern können. Hier lohnt es sich, in die Trickkiste zu greifen. Zunächst der Code:

button {
    /* removed transition: background-color */
    position: relative;
}

button::after {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    background-color: #fff;
    opacity: 0;
    transition: opacity 0.3s;
}

button:hover::after {
    opacity: 0.1;
}

Folgendes hat sich nun geändert:

  • Zunächst hat der Button keine Transition mehr. Sie wird durch position: relative ersetzt.
  • Anschließend nutzt man das Pseudo-Element after, um in dem Button einen absoluten Kasten einzufügen, der sich über die gesamte Größe des Buttons erstreckt. Dieser Kasten hat eine weiße Hintergrundfarbe und ist zunächst nicht sichtbar.
  • Wenn der Button nun gehovert wird, ändert sich die opacity des Kastens. Da er eine Transition hat, wird dieser Kasten nun über dem Button gezeigt. Er simuliert so das Ändern der Hintergrundfarbe. Den Button selber kann man immer noch sehen, da der Kasten nur eine leichte Deckkraft hat.

So erhält man eine performante Animation, mit der sich Farben „ändern“ können. Und auch wenn dieser Form der Optimierung nicht in jedem Fall unerlässlich ist, kann sie doch gerade bei komplexen Animationen zu einem guten Ergebnis führen.

Wie lässt sich der Erfolg einer Animations-Optimierung kontrollieren?

Ob die Optimierung einer Animation tatsächlich den gewünschten Effekt hat, lässt sich mit Developer-Tools in Chrome kontrollieren. Sie helfen bei der Entwicklung von Animationen und können in Chrome zum Beispiel mit F12 geöffnet werden.

Einige Fenster in den Developer Tools, die nicht per Default geöffnet sind, lassen sich hier finden:

Um die Performance der Animationen mithilfe des Fensters Performance in den Developer-Tools zu untersuchen, sollte man auf Aufnehmen klicken, anschließend die Animation abspielen und dann die Aufnahme beenden. Für die nicht optimierte Animation des Side-Menüs liefert das Tool dann folgendem Bericht:

In den rot umrandeten Bereichen lassen sich die wichtigsten Informationen ablesen:

  1. Um das Ergebnis besser zeigen zu können, wurde die CPU 6x langsamer gemacht.
  2. Bei den Frames ist zu erkennen, dass die grünen Balken unterschiedlich dick sind. Die Berechnung für einzelne Frames schwankt stark. Das fällt besonders im rechten Bereich auf.
  3. Der Browser ist bei einer einsekündigen Animation fast 400ms damit beschäftigt das Menü zu rendern. Weitere 160ms sind dem Painting zuzurechnen. Wie bereits erwähnt: Bei der nicht optimierten Version der Seite muss der Browser immer wieder Layout, Paint und Composite durchlaufen.

Der Bericht für die optimierte Animation sähe demgegenüber so aus:

Die Frames zeigen konstant 60 FPS und der Unterschied bei den Zusammenfassungen ist gigantisch. Der Browser muss in der optimierten Version viel weniger Rendering und Painting betreiben.

Ebenfalls nützlich ist das Fenster Rendering mit der Option Paint Flashing. Wenn sie aktiv ist, blinken Bereiche der Seite, die erneut ein Painting erhalten. Bei der inperformanten Variante der Side-Menüs ist das Blinken im gesamten Verlauf der Animation zu sehen, bei der optimierten Version nur am Ende der Animation.

Als weiteres hilfreiches Tool soll an dieser Stelle auch das Animations-Fenster erwähnt werden. Damit können Animationen in verschiedenen Geschwindigkeiten abgespielt werden, wodurch fehlende Frames oder eine schlechte Performance deutlich sichtbar werden.

Die genannten Tools können auf dieser Seite ausprobiert werden: https://niklassy.github.io/animation-performance/index.html

Zusammenfassung

Zusammenfassend lassen sich folgende Kernaussagen zur erfolgreichen Entwicklung einer performanten Animation festhalten:

  • Am besten nur transform und opacity animieren.
  • RequestAnimationFrame für JavaScript Animationen verwenden.
  • Bedenken, dass weniger meist mehr ist.
  • Will-change nur als letzte Möglichkeit der Optimierung nutzen.

Während des Prozesses der Entwicklung ist es außerdem wichtig, die Animationen auf verschiedenen und vor allem mobilen Endgeräten zu testen. Um zu testen, ob eine Animation optimal läuft, bieten sich die Developer-Tools in Chrome an. Am wichtigsten sind hier die Fenster Rendering, Performance und Animations.

Referenzen


Posted

in

, ,

by

Niklas Brocker

Tags:

Comments

Leave a Reply