search home list

Web Performance auf intu.io: Von 0 auf 100 in 3,7 Sekunden

Links die aktuelle Seite über einen Browser in London geladen, rechts der alte Stand. Dazwischen 4 Sekunden.

Soweit man das archäologisch nachvollziehen kann, ist intu.io in seiner jetzigen Form fast 5 Jahre alt. Das ist im Internet eine sehr lange Zeit und wir sind fast ein bisschen stolz, dass man der Seite ihr Alter nicht ansieht. Trotzdem hat sich in der Zwischenzeit viel getan und vor allem die Ansprüche an Web Performance sind höher geworden. Es war also an der Zeit, unsere Website technisch zu überholen und schneller zu machen.

Der erste Halt bei einem solchen Vorhaben sind Googles PageSpeed Insights und der WebPageTest. Beide bestätigten in unseren Tests, dass jede Menge Raum für Verbesserungen bleibt: Von einem Londoner Knoten aus gemessen brauchte die Seite gut 4,5 Sekunden, bis der Browser zu rendern beginnt und PageSpeed Insights vergibt 74/100 Punkte für mobile Endgeräte. Nach unseren Verbesserungen konnte der Browser schon nach 0,5 Sekunden rendern und auch Googles mobile Heuristik belohnt die Optimierungen mit 98/100 Punkten. Das ist eigentlich nur einigen wenigen Änderungen zu verdanken, die wir hier kurz erklären wollen.

Statisch werden

Die erste signifikante Verbesserung setzt auf dem Server an, denn alle Analysen zeigten klar, dass die Time-To-First-Byte (TTFB), also die Server-Antwortzeit für den ersten Request, wesentlich zu hoch ist. WebPageTest kommt hier auf 3 Sekunden, Tests vor Ort in Wien benötigen mindestens eine Sekunde. So lange dieser Wert nicht kleiner wird, greifen alle weiteren Verbesserungen nicht wirklich, da der Browser ohne Daten verständlicherweise nicht voran kommt. Weil alle statischen Assets schnell ausgeliefert wurden und der DNS-Request zügig läuft, liegt der Verdacht nahe, dass WordPress bzw. PHP hier die Auslieferung trotz Caching verzögern. Weil auf der Seite außer dem Twitter-Feed nicht viel dynamisch ist und wir gerne Gulp-Toolchains bauen, war der erste Schritt also von PHP-Templates auf statisches HTML zu migrieren.

Die Twitter-Nachrichten werden aktuell immer noch in PHP vorbereitet, aber dann asynchron über AJAX via JSON Feed in die Seite eingefügt, PHP ist also komplett aus dem kritischen Pfad entfernt. Mit dem Erfolg, dass das erste Byte jetzt in unter 300 ms in London ankommt.

Time To First Byte
Der Wechsel von PHP hin zu einer statischen Seite reduziert die Time-To-First-Byte dramatisch (oben: vorher, unten: nachher). Man sieht jeweils den Redirect auf die englische Version der Seite.

Eine Herausforderung beim Tooling war für uns die englische Übersetzung in der statischen Seite. Es macht keinen Sinn, zwei Varianten der index.html parallel in zwei Sprachen zu pflegen, wir brauchen also eine Lösung, die ohne Mehraufwand eine lokalisierte englische Variante erzeugt. Genau das leistet gulp-l10n, das automatisch Strings in HTML-Dateien erkennt und daraus eine JSON-Datei erzeugt, das dann wiederum zum Übersetzen in andere Sprachen benutzt werden kann:

de.json

{
  "a7bfcebb": "Das intuio Team",
  "52e7f683": "Das sagen unsere Kunden:"
}

en.json

{
  "a7bfcebb": "the intuio team",
  "52e7f683": "What our customers say:"
}

So wird die englische Version der Website als statisches HTML über Gulp erzeugt, und zwar ohne, dass jede Menge Keys manuell verwaltet und gepflegt werden müssen. Bei jedem neuen oder veränderten String warnt der Gulp-Build, dass die englische Version nicht länger vollständig ist. Ausgeliefert wird die jeweilige Seite dann gemäß der im Browser eingestellten Sprache via .htaccess Redirect.

Critical Path

Dank der statischen Website kommt das erste Byte jetzt schnell zum User – bis der Browser nach dem ersten Byte zu rendern beginnt, verstreichen bei der ursprünglichen Seite aber immer noch fast 1,5 Sekunden. Der zweite Schritt war deshalb, die nötigen Ressourcen im Critical Path, also dem Weg zum ersten Rendern zu weit wie möglich zu reduzieren. Nach Möglichkeit soll schon mit einem HTTP-Request genug Information beim Browser ankommen, um dem User etwas zu zeigen. Blockiert wird dieser Pfad von Fonts (zumindest teilweise), Javascript und CSS, da diese Ressourcen standardmäßig das Rendern aufhalten bis sie geladen und vom Browser interpretiert sind.

Javascript

In Javascript lässt sich die Blockierung relativ leicht umgehen, indem man alle Ressourcen asynchron lädt. Dafür genügt ein entsprechender async Parameter im Script-Tag:

<script type="text/javascript" src="/_js/app.js" async></script>

Um hier keine Requests zu verschenken, enthält app.js gebündelt und minifiziert alle Javascript-Dateien die für die Website notwendig sind. Weil wir mittlerweile 2016 schreiben und auf der Seite moderne Browser auf relativ wenig Javascript treffen, haben wir dabei jQuery gleich rausgeworfen und damit knapp 80kb (vor Gzip) gespart.

Übertrage Bytes Von jQuery auf vanillaJS zu wechseln spart deutlich Javascript-Code (rot: vorher, blau: nachher)

Fonts

Fonts sind etwas komplexer, da sie von Haus aus noch keinen Mechanismus mitbringen, sich asynchron laden zu lassen und dabei intelligent Fallbacks einzublenden. Das wird sich in Zukunft mit dem font-display descriptor ändern, aber im Moment braucht es noch handgestrickte Lösungen.

Wenn man die ursprünglich auf intu.io eingesetzten Fonts analysiert, gibt es da zwei Gruppen: Einerseits die Schriften von Typekit und andererseits einen Iconfont. Typekit erlaubt, das eigene Javascript asynchron einzubetten und lädt damit auch die angeforderten Schriften non-blocking. Das kann zwar dazu führen, dass Text-Blöcke im Ladeprozess ihre Schriftart wechseln (FOUT – Flash Of Unstyled Text), aber die Alternative wäre, die selben Blöcke bis zum Laden der Schrift unsichtbar darzustellen (FOIT – Flash Of Invisible Text). Asynchrones Nachladen von Fonts ist also definitiv die bessere Lösung für positive User Experience.

Font-Icons haben in den letzten Jahren allgemein einen schlechten Ruf bekommen, da sie im Vergleich zu SVGs wesentlich schlechtere Accessibility bieten und notorisch schwer auszurichten sind. Die Entscheidung den Icon-Font in ein SVG Sprite umzuwandeln war also leicht.

Dieser Wechsel kommt allerdings nicht ohne eigene Probleme, weil nur ins HTML eingebette SVGs über globales CSS dargestellt werden können. Wenn sich SVG-Icons also analog zu Icon-Fonts an die Schriftfarbe ihres unmittelbaren Kontextes anpassen sollen, benötigen wir deshalb in das HTML eingebettete Icons und die magische CSS-Eigenschaft currentColor, in der immer die Schriftfarbe des Eltern-Elements vererbt wird:

.icon {
  fill: currentColor;
}
Die Icon-Klasse wird auf allen SVG-Icons angebracht

Mit dieser Klasse werden alle Pfade im SVG in der aktuellen Schriftfarbe gefüllt und SVG-Icons passen sich farblich sauber ein. Mehrfarbige SVGs verlieren damit natürlich ihre Farben und brauchen deshalb eine andere Klasse. Damit SVGs nicht bei jeder Verwendung wieder neu zum Template hinzugefügt werden müssen, gibt es einen sehr bequeme Sprite-Ansatz, bei dem sich Bilder nicht wie früher über ihre relative Position im Sprite, sondern per ID referenzieren lassen. Das Sprite haben wir übrigens mit icomoon.io generiert.

Sprite

<svg style="position: absolute; width: 0; height: 0;" width="0" height="0" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <defs>
    <symbol id="icon-pencil" viewbox="0 0 24 28">
      <title>pencil</title>
      <path d="M5.672 24l1.422-1.422-3.672-3.672-1.422 1.422v1.672h2v2h1.672zM13.844 9.5q0-0.344-0.344-0.344-0.156 0-0.266 0.109l-8.469 8.469q-0.109 0.109-0.109 0.266 0 0.344 0.344 0.344 0.156 0 0.266-0.109l8.469-8.469q0.109-0.109 0.109-0.266zM13 6.5l6.5 6.5-13 13h-6.5v-6.5zM23.672 8q0 0.828-0.578 1.406l-2.594 2.594-6.5-6.5 2.594-2.578q0.562-0.594 1.406-0.594 0.828 0 1.422 0.594l3.672 3.656q0.578 0.609 0.578 1.422z"></path>
    </symbol>
  </defs>
</svg>

Icon

<svg class="icon">
  <use xlink:href="#icon-pencil" />
</svg>

Der Nachteil dieser Inline-Sprite-Technik ist, dass die Icons damit Teil des kritischen Pfades werden, auch wenn sie auf intu.io erst relativ weit unten auf der Seite erscheinen. Da diese Icons zusammen nur auf wenige Kilobyte kommen, ist das aber gut verkraftbar. Im Sprite sieht man außerdem, dass die Icons hier ein Title-Tag enthalten, das computerlesbare Informationen zum Inhalt des Icons enthält. Für Screen-Reader ist das Icon somit nicht länger unsichtbar.

CSS

Javascript und Fonts lassen sich auf intu.io komplett aus dem kritischen Pfad entfernen, aber CSS wird benötigt, um dem Benutzer gut darstellbare Inhalte zu zeigen. Zumindest ein bisschen CSS. Unser Ansatz war deshalb, das bestehende CSS in kritische und unkritische Teile zu splitten und das kritische CSS sofort auszuliefern, während der Rest erst nach dem ersten Rendern nachgeladen wird.

Zu diesem Zweck gibt es eine Reihe von Tools die auf Basis eines vordefinierten Folds, also der Untergrenze des Browser-Viewports, alles kritische CSS extrahieren. Kritisch ist hierbei alles, was zur Darstellung oberhalb des Folds notwendig ist.

Wir haben uns beim Umbau von intu.io gegen solche Tools entschieden, da diese ihre schnelle Handhabung mit einer Reihe von Problemen erkaufen: In der Regel wird ein Headless Browser wie PhantomJS gestartet und ausgewertet, welches CSS überhalb des Folds wirkt. Das schränkt die Analyse allerdings auf nur einen Browser ein, Firefox und IE könnten also etwas anderes CSS benötigen. Die Tools, die wir uns zu diesem Zweck angeschaut haben, zwingen den User überdies, das extrahierte kritische CSS später im Gesamtblock noch mal zu laden. Das hat den Vorteil, dass im Endeffekt die Reihenfolge aller CSS-Regeln gleich bleibt und es keine Bugs durch veränderte Style-Prioritäten geben kann. Es bedeutet aber auch, dass ein guter Teil des CSS doppelt geladen wird.

Weil unser CSS sowieso schon aus Sass-Modulen generiert wird, haben wir uns deshalb für den manuellen Ansatz entschieden, der zwar mehr Arbeit bereitet, aber auch mehr Kontrolle und weniger Redundanzen bringt. Der grundsätzliche Ansatz bleibt dabei der gleiche: im critical.css werden alle Module zusammengefasst, die für den oberen Teil der Website gebraucht werden und genug, dass der Bereich darunter sich seinem Endzustand zumindest annähert. Alles weitere wird von Gulp in eine zweite Datei gepackt, die erst nach dem ersten Rendern greift. Dazu nutzen wir den Preload-Ansatz von loadCSS:

<link rel="stylesheet" href="dist/_css/critical.css" type="text/css" media="screen" inline>
<link rel="preload" href="/_css/base.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/_css/base.css"></noscript>

Unterstützt der Browser Preload, wird das nicht-kritische base.css non-blocking geladen und erst nach dem Onload-Event als Stylesheet deklariert. Die blockierende Auswertung des Stylesheets passiert also erst nachdem der User schon den kritischen Bereich sieht. Browser die kein Preload unterstützen, bekommen als Teil des gebündelten Javascripts ein Polyfill, sowie loadCSS, um die gleiche Funktionalität zu erreichen. Browser komplett ohne Javascript laden das CSS über das Noscript-Tag.

In dem Code-Beispiel ist eine weitere Performance-Verbesserung integriert: Wenn critical.css als externe Resource geladen wird, erzeugt das einen neuen HTTP-Request, der bei HTTP1.1 bekanntermaßen noch teuer ist (HTTP2 wird das in naher Zukunft ändern). Deshalb läuft als Teil des Gulp-Toolchains Gulp-Inline-Source über den HTML-Code und integriert alle Scripts, CSS oder Bilder, die den Parameter inline enthalten, ins HTML Template. Auf intu.io ist das neben dem kritischen CSS auch das Logo als SVG. Damit packen wir alles für eine gute Darstellung der Seite notwendige in nur einen HTTP-Request und der Browser kann nach weniger als 500ms schon mit dem Rendern beginnen.

Modernizr

Ein unerwartetes Hindernis im Critical Path war Modernizr. intu.io setzt viele CSS-Animationen ein, um den Seitenaufbau schöner zu gestalten. Dieser CSS-Animationscode war ursprünglich von der Klasse .cssanimations anhängig, damit er auch wirklich nur auf Browsern eingesetzt wird, die CSS-Animationen unterstützen. Diese Klasse wird von Modernizr auf das HTML-Tag gesetzt, sobald der Browser erfolgreich auf seine Animationsfähigkeit getestet wurde. Für den kritischen Pfad zur Darstellung bedeutet das allerdings, dass der Seitenaufbau erst dann animiert, wenn das Javascript geladen und ausgeführt wurde. Der kritische Pfad muss also doch wieder auf unkritische Ressourcen warten und macht damit einen unnötigen Umweg. Die Lösung war einfach: Animationen auch ohne Modernizr bzw. die .cssanimations Klasse starten – Browser die keine Animationen unterstützen haben zum Glück den Anstand, sie einfach zu ignorieren.

Bilder

Ein kurzer Critical Path zeigt dem Benutzer schnell eine brauchbare Website, aber damit ist der Render-Prozess in der Praxis noch nicht zu Ende, denn auch die unkritischen Ressourcen wollen irgendwann geladen werden. Schaut man sich diese unkritischen Daten an, bestehen diese (sowohl vor als auch nach der Optimierung) zu einem guten Teil aus Bildern. Um intu.io hier schneller zu machen, war es also wichtig, das übertragene Bild-Volumen so weit wie möglich zu verkleinern.

Requests und Bytes

Welche Auflösung für ein Bild optimal ist, ändert sich allerdings je nach Größe und Pixeldichte des Devices stark. Für optimale Web Performance braucht es also responsive Bilder. Je nach Position haben wir diese responsive Bilder via Media-Queries im CSS (bei Hintergründen) oder mit dem Picture-Tag umgesetzt. Beim Picture Tag sind wir dabei auf eine besondere Situation gestoßen: Einige Bilder sollen in kleinen Breakpoints nicht anders, sondern gar nicht geladen werden. Ein display:none via CSS löst das Problem nicht wirklich, weil der Browser bereits vor dem Auswerten des CSS beginnt Bilder zu laden – wir haben deshalb in dieser Situation ein base64-codiertes 1x1px GIF eingebunden:

<picture class="hidden-phone">
  <source media="(min-width: 37.5em)" srcset="/_images/01_home/chair.svg" type="image/svg+xml" />
  <img class="tagline__img" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="So intuitiv benutzbar und komfortabel wie Ihr Lieblingssessel.">
</picture>

Auf Breakpoints unter 37.5em lädt der Browser also gar kein Bild, sondern zeigt nur ein transparentes Pixel an und lässt uns in Erinnerungen an die 1990er und das spacer.gif schwelgen. Obwohl das Bild nur 1px hoch ist, bläst Chrome das Picture-Tag allerdings auf die Standard Zeilenhöhe von 18px auf. Das Bild ist also noch durch eine .hidden-phone Klasse auf kleinen Breakpoints versteckt, damit es das Layout nicht verschiebt.

Das Beispiel zeigt auch gleichzeitig, dass wir an vielen Stellen jetzt auf SVGs setzen, die unabhängig von der Pixeldichte des Displays immer perfekte Ergebnisse liefern und zudem in der Regel (aber nicht immer) wesentlich kleiner sind, als das entsprechende Pixelbild. Zumindest, wenn man sie mit svgo optimiert.

Altlasten entfernen

Wie bei allen Optimierungen greift auch bei der Web Performance irgendwann der abnehmende Grenznutzen. Wir konnten durch Überarbeitung fast 50% des CSS einsparen, in kB gemessen macht das ganze aber nur einen relativ geringen Unterschied und wirkt sich deshalb auch nur begrenzt auf die Performance aus. Trotzdem wollen wir euch die Optimierungen natürlich nicht vorenthalten.

Der erste Punkt sind Vendor-Prefixe. Diese wurden ursprünglich von der Sass-Library Compass generiert und waren auf dem Stand vor 5 Jahren, als es noch deutlich mehr Prefixes gab, weil sich die Idee des Feature-Flags bei neuen Browser-Entwicklungen noch nicht durchgesetzt hatte. Damit nur die wirklich nötigen Prefixes generiert werden, haben wir die Aufgabe von Compass an PostCSS bzw. den Autoprefixer übergeben. Dieser CSS-Prozessor kennt den aktuellen Stand von caniuse.com und erzeugt so nur die Vendor-Prefixes, die aktuell gebraucht werden. In unserem Fall für die jeweils zwei aktuellsten Browser jedes großen Herstellers. Das hat auch den Vorteil, dass sich der Sass-Code normalem CSS wieder deutlich mehr annähert. Wie bei Javascript auch, bedeutet unserer Meinung nach zukunftssichere Entwicklung, aktuellen Code eher durch Polyfills bzw. Transpiler an neue Standards anzupassen, als komplett browser-unabhänge Lösungen zu entwickeln. Mittelfristig müssen wir also wohl unsere geliebten Sass-Variablen durch CSS Custom Properties ersetzen.

Eine unnötige Altlast waren außerdem die diversen Workarounds für IE8, für den bei jeder Größenangabe in rem automatisch ein Fallback in px generiert wurde und es spezielle .no-mq Klassen gab, die für IE6-8 den Desktop-Breakpoint ausliefern. Weil IE8 in unseren Nutzer-Statistiken überhaupt nicht mehr vorkommt, konnten diese Fallbacks ebenfalls komplett entfernt werden.

Visueller Aufbau der Seite als Kennzahl für Web Performance Visueller Aufbau (rot: vorher, blau: nachher). Der Aufbau wird duch den Einsatz von Animationen noch etwas herausgezögert. Aber wir optimieren am Ende dann doch für User Experience und nicht für Sekundenwerte.

Pagespeed: Was bleibt

Zum Schluss noch ein Wort zu den PageSpeed Insights, die uns zu Beginn der Optimierung richtig die Hauptschwachpunkte der Seite aufgezeigt haben. Anders als der WebPageTest werden hier keine echten Performance-Daten gemessen, sondern nur heuristisch nach gewissen Merkmalen gesucht. Im Laufe der Optimierung hat sich gezeigt, dass diese Heuristiken nicht immer ganz aktuell sind, besonders bei der Desktop-Variante des Tests. Im Gegensatz zur mobile Variante bemängelt dieser weiter, dass blockierende CSS-Daten geladen werden. Vermutlich, weil der Preload-Trick von loadCSS noch relativ jung ist. Auch wenn PageSpeed also ein guter Ausgangspunkt ist, führt bei ernsthafter Optimierung kein Weg an harten Daten und damit WebPageTest vorbei. Auch dieses Tool reduziert seine Analysen übrigens am Ende auf eine Zahl, den sogenannten Speed Index, der aussagt wie schnell eine Seite visuell vollständig ist. Bei intu.io liegt diese Zahl jetzt nicht mehr bei 5000, sondern ist auf 911 geschrumpft. Ein Ergebnis mit dem man zufrieden sein kann. Zumindest für den Moment, bis unser Hoster HTTP/2 unterstützt und es Zeit für eine neue Optimierungsrunde ist.

Show all articles

One Response

  1. […] Ein oft vergessener aber immens wichtiger Faktor für eine gute UX ist Performance. Björn Ganslandt von Intuio zeigt am Beispiel des eigenen Firmen-Blogs, mit welchen technischen Kniffen, die Ladezeiten massiv gesenkt werden konnten. […]

What do you think?