Website UX Performance Toolkit / Plain Tweaks

UX Performance betrifft ein gesamtes Projekt – vom HTML, CSS und Javascript über den Browser bis zum Besucher der Webseite. Neben der schnellen Auslieferung einer Website (HTTP2, Komprimierung, Minifizierung, Above The Fold / Critical CSS, Caching Header und und und) steht und fällt die Benutzer Erfahrung (das UX – User Experience), sozusagen das Gefühl wenn man die Seite nutzt, mit der Art und Weise wie flüssig ein Browser eine Seite dauerhaft darstellen kann.

Optimierungen an der gefühlten Performance sind im Nachhinein immer schwieriger, als wenn man direkt bei der Implementierung alle Stellschrauben im Blick hat. Die folgende Liste gibt ein paar Anhaltspunkte auf die man achten kann, während der Umsetzung eines Webprojektes.

Die Liste geht bis auf wenige Ausnahmen nicht auf Bibliotheken oder Frameworks ein, sondern auf Frontend Plain-Tweaks, also ein Teil der Werkzeugkiste die man im Hinterkopf haben sollte, ohne npm, bower und co.

TL;DR

  1. Animationen und Übergänge auf einer Webseite mit CSS3 animate oder transition realisieren
  2. absolute und fixed Elemente per transform: translate() animieren, nicht per top, left, right, bottom
  3. Bei Änderungen der Anzeige Art von Elementen (von absolute zu fixed etc) den frei werdenden Platz vorher schon mit padding reservieren
  4. Im Javascript alle DOM-Queries cachen
  5. Javascript Handler delegieren
  6. DOM Elemente selten löschen / hinzufügen
  7. Idealerweise fastdom für DOM Manipulationen benutzen
  8. In teuren Listener wie scroll, mousemove oder resize wenig direkt auf den DOM zugreifen
  9. Teure Listener verlagern
  10. Resize Listener mit absoluter Vorsicht benutzen
  11. will-change oder contains einsetzen
  12. tbc.

Animation und Übergänge mit CSS3 realiseren

Vor CSS3 musste man auf Javascript zurückgreifen um Dinge auf einer Webseite zu animieren – ein fadeIn/slideDown/slideUp usw… Am besten noch mit einer Ease-Methode, da lineare Interpolationen sich so unmenschlich anfühlen.

Mit CSS3 kann man einen Großteil der Animationen und Übergänge direkt im CSS definieren. Das hat den Vorteil, dass der Browser diese Abläufe besser in seine eigene Render Abläufe integrieren und somit seine Arbeit besser planen kann. Jeder der mal versucht hat einen MousOver/MouseOut Effekt in Javascript zu programmieren, wird die CSS Umsetzung mit :hover und :focus Pseudo Klassen zu schätzen gelernt haben, da der Browser auch besser weiß, welche Hitbox getroffen wird.

Trotzdem, animate oder transition alleine bringen es noch nicht. Ein transition: all .33s ease-in-out; habe ich nicht nur einmal gesehen, was mich immer mit der Frage zurück lässt ob der Entwickler tatsächlich jede animierbare Eigenschaft bei einer Veränderungen interpolieren will – oder ob ein erster Versuch im Endprodukt gelandet ist.

transition: background-color .33s ease-in-out, color .33s ease-in-out, padding-top .33s ease-in-out;

Das ist zwar mehr Schreibarbeit, das Ergebnis ist aber bestimmbarer.

Um Optimierungspotential zu sehen bietet sich an in den Chrome Dev Tools Paint flashing, FPS Meter und Scrolling Performance Issues zu aktivieren.

absolute oder fixed animieren

Etwas konkreter sind Fälle, in denen ein Element absolute oder fixed positioniert ist, und man es verschieben will.

Das gleiche visuelle Ergebnis kann auf eine performante und eine unperformante Weise erreicht werden. Animiert man bei einem Flyout/Flyin Menü die top/left Position, zwingt man den Browser zum ständigen neurendern (Layout Thrashing/Jank Free). Transitioniert man stattdessen das Flyout/Flyin per transfrom: translate(100%, 0); wird nur der Layer selbst verschoben und nichts weiter von der Seite neuberechnet.

Ein kleiner Vorgriff: Mit will-change gibt man dem Browser einen Hinweis, in wie fern sich bestimmte Elemente verändern werden. Wie man es richtig nutzt kann man hier sehen: will-change: on und will-change: off.

Sticky Navs & Co

Elemente die man während des Scrollens in ihrer Positionierung ändert, zum Beispiel wenn diese auf einmal sticky werden, sollten so realisiert werden, dass die verbleibenden Elemente schon vorher auf die Lücke vorbereitet sind.

Angenommen man baut eine Navigation die kurz mit scrollt um dann sticky zu werden, kann man dem <body> ein entsprechendes padding-top mitgeben. Wenn das Element dann sticky oder unsticky wird, müssen die nachfolgenden Elemente nicht aufrücken, oder per CSS Klasse an ihrer Position gehalten werden. Der Übergang zwischen sticky und unsticky wird so sanft.

DOM Queries cachen

Ein PlainJS-Entwickler wird aus purer Faulheit jegliche DOM-Queries cachen, also in einer Variable speichern. Ein Bling ($) JS-Entwickler nicht unbedingt, die Kurzschreibweise $('.my-element') ist leider verführerisch.

Was man sich bewusst machen muss, ist das bei jedem $('[data-click-track-me]') im Hintergrund ein document.querySelectorAll('[data-click-track-me]') ausgeführt wird. Bei komplexen/unperformanten Selektoren (z.B. [data-is="on"]) und großen Seiten kann sich dies durchaus bemerkbar machen.

var $header = document.querySelector('[data-header]'); oder var $header = $('[data-header]'); und danach arbeitet man nur noch mit dieser Variable. Änderungen die man auf dem gecachten Element ausführt, sind auch dort greifbar – ein document.querySelector('.xyz') liefert einen dauerhaften Zeiger auf das Element zurück, keine Momentaufnahme.

Event Delegation

Ein Punkt, der im Plain Business etwas schwieriger ist, als im Bling ($) Business. Mit jQuery ist es sehr einfach möglich Event Handler zu delegieren. Dazu habe ich schon mal etwas geschrieben: jQuery vs Vanilla JS featuring JSLint = good practice.

Die Idee dahinter ist, dass man einen Eventlistener auf einem Elternelement installiert, und dieser verarbeitet dann zum Beispiel die click Ereignisse der Kind Elemente. Bei einem Akkordeon setzt man den Click Listener nur auf das umschließende Element, anstatt auf jeden Akkordeon Kopf.

Ein lesenswerter Artikel (mit Kommentaren) ist dieser hier: event-delegate.

DOM Elemente hinzufügen / löschen

Jede DOM Manipulation (lesen und schreiben) verunsichert den Browser. Das simple setzen einer Klasse kann einen riesigen Render Aufwand bedeuten. Genauso ist es wenn man Elemente dem DOM hinzufügt oder wegnimmt. Hier ist die Empfehlung möglichst wenig Änderungen am DOM vorzunehmen. Möchte man zum Beispiel eine unordered list hinzufügen, so muss diese Liste bevor sie in den DOM eingehängt wird, komplett zusammengebaut sein – also eine Änderung.

Würde man erst das <ul> Element dem DOM hinzufügen und danach sukzessiv die <li> Elemente, müsste der Browser immer wieder alles neu berechnen. Hierbei würde man auf jeden Fall aus einem AnimationFrame herausfallen – im Beitragsbild die roten Blöcke in der Timeline Ansicht.

fastdom

Die fastdom Bibliothek unterstützt einen bei DOM Manipulationen. Ein Browser mag weder lesenden, noch schreibenden Zugriff auf seinen DOM. Die UX-Performance leidet noch mehr, wenn man abwechselnd in den DOM schreibt und liest.

Idealerweise liest man erst alles lesenswerte Werte aus dem DOM, bevor man etwas in den DOM schreibt – immer.

Endlich kommt fastdom ins Spiel. fastdom unterstützt genau an dieser Stelle, in dem es alle lesenden und schreibenden Zugriffe auf den DOM bündelt – und sei es nur dass eine Klasse gesetzt wird. Mit der asynchronen Funktion fastdom.measure(function () { /* read DOM things here */ }); und fastdom.mutate(function () { /* manipulate DOM things here */ }); werden lesende und schreibende Vorgänge geordnet und im nächsten requestAnimationFrame ausgeführt. Zu diesem Zeitpunkt startet der Browser gerade einen neuen Render Durchlauf und ist bereit für Änderungen jeglicher Art – bzw. man grätscht nicht einfach dazwischen. Der requestAnimationFrame Callback wird alle 60 Hertz ausgeführt, ca. jede 16.6ms.

Noch entspannter ist der Browser nur zum Zeitpunkt des requestidleCallback – aber dieser ist unberechenbarer.

Wieso hat jetzt fastdom oder requestAnimationFrame nochmal Vorteile?

Alle 16.6ms beginnt der Browser die Anzeige neu zu berechnen. fastdom und requestAnimationFrame setzen sich genau an den Start dieses Zeitpunktes. Ohne diese Synchronisation, grätscht man mit jeder DOM Manipulation irgendwo rein, und der Browser muss wieder von vorne anfangen. Diese Momente provozieren ruckeln und andere Unannehmlichkeiten in der Benutzererfahrung.

Teure Listener

Manche Events sind teurer als andere. Events die mit Bewegung / Verändernug der Seite zu tun haben sind teuer. scroll oder mousemove Events finden in extremer Häufigkeit statt. Ein resize Event (zum Beispiel die Orientierungsänderung eines mobilen Gerät) zwingt den Browser alles neu zu berechnen und löst sogar ein Scrollevent aus, da die Breitenänderung auch wie ein Scrollevent zu handeln ist.

Wenn man sich an diese Events klammert, muss man immer unterscheiden ob man diese Events in Echtzeit verarbeiten muss, ob man sie später verarbeiten kann oder ob man vielleicht nur den Start oder das Ende des Events verarbeiten muss.

Im Fall, dass man diese Events in Echtzeit behandeln muss, müssen DOM Zugriffe lesend wie schreibend genau überlegt sein. Beim Scrollen braucht man wahrscheinlich immer die neue Scrollposition die direkt vom window gelesen werden muss – andere Werte, ob zum Beispiel ein Element in den sichtbaren Bereich gewandert ist, muss in reinem Javascript gelöst werden, also bei der Initialisierung der Scoll-Magic alle Werte (Höhe, Offset & Co) lokal im JS gespeichert werden. Bei einem Resize Event müssen diese Werte neu ausgelesen werden (optimal verlagert aka debounced!).

Spätestens auf mobilen Geräten kann diese Art der teuren Event Behandlung das UX optimieren.

Resize Listener

Wie schon beschrieben – ein Resize Event hat immer zur Folge, dass der Browser die komplette Seite neu rendern muss und danach sogar noch ein Scroll Event feuert. Wenn man hiermit ungeschickt umgeht kommt jede Seite ins Stottern. Im besten Fall verarbeitet man Resize Events nie direkt.

will-change & containment

will-change

Die CSS Eigenschaft will-change teilt dem Browser mit, dass mit dem entsprechenden Element eine Veränderung vorgenommen wird. Der Browser wird dann versuchen, dieses Element in einen eigenen Compositing-Layer zu stecken und das Layout-Thrashing zu umgehen.

Ein Browser versucht auch ohne diese Eigenschaft schon alles zu optimieren, daher sollte man diese Eigenschaft nur mit Bedacht setzen. Paul Lewis und Das Surma haben in ihrer Live Coding Session die Eigenschaft will-change immer nur solange gesetzt, wie die Animation/Transition auch stattfindet.

containment

Die CSS Eigenschaft contains: strict; teilt dem Browser mit, dass egal was innerhalb dieses Elements geändert wird, dies keine Auswirkungen auf andere Elemente hat. Eine Animation der Schriftgröße wird die Bounding Box des Elements nicht mitwachsen lassen, wodurch der Rest der Seite nicht neu gerendert werden muss.

Das ist ziemlich cool, aber Stand jetzt (22.04.2017) nur im Chrome implementiert.

Fazit

Bei größeren oder sehr fancy Projekten passiert es schnell dass man mit besten Vorsätzen startet und am Ende eine ruckelnde und stotternde Seite hat, die vielleicht sogar den Browser abstürzen lässt. Die oben genannten Anhaltspunkte sind für mich mittlerweile feste Größen ab der ersten Zeile Code/CSS die ich schreibe, ohne Unterschied ob es ein simples Flyout ist, ein modaler Dialog oder eine onscroll Animation mit Video Hintergrund.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.