Wenn in der Web Performance Optimierung vom „kritischen Rendering-Pfad“ (Critical Rendering Path) gesprochen wird, dann bedeutet das, dass für den aktuellen Viewport (meist der „above the fold“ Viewport, bevor der User scrollt) alle benötigten Dateien und Elemente vom Browser so schnell wie möglich geladen und verarbeitet werden. Der „Pfad“ ist hier die Kette an Events, die der Browser dafür ausführen muss – sprich HTML laden und parsen, Inline-CSS/-JS, sowie externe Ressourcen laden und verarbeiten, zudem Bilder, Fonts, vielleicht Videos und und und…
Zusammenfassend macht der Browser folgendes im Hintergrund, um die Webseite anzuzeigen:
-
- Document Object Model (DOM)
- CSS object model (CSSOM)
- Render Tree
- Layout
- Paint
Wer tiefer in das Browser-Rendering einsteigen will, dem empfehle ich diesen Artikel über die Umstellung der Firefox Rendering Engine.
Um diesen Prozess für den Nutzer so kurz wie möglich zu halten, haben Frontend-Entwickler mittlerweile neue Möglichkeiten.
Für die Optimierung kommen mehrere Bereiche in Frage, die zu analysieren und optimieren sind:
- JavaScript asynchron laden
- Critical CSS generieren und weitere Stile asynchron laden
- Kritische Webfonts identifizieren und vorab laden
Wer sich schon ein wenig mit der Web Performance Optimierung beschäftigt hat, wird gehört haben, dass es grundsätzlich empfohlen wird, CSS-Dateien im `<head>` und JavaScript am Ende der HTML-Datei zu referenzieren. Das stimmt soweit. Der Browser hat einige Algorithmen, die das Verarbeiten der Inhalte priorisieren kann, und es gibt ein Default-Verhalten, das – um es vorweg zu nehmen – seit jeher gleich und nicht optimal ist. Heutzutage haben wir in den Browsern mehr Möglichkeiten, um das Ladeverhalten stark zu verbessern.
Im Allgemeinen führt der Browser kein Rendering auf dem Bildschirm aus, wenn er noch Ressourcen aus dem `<head>` lädt – man spricht hier vom Render-Blocking. Das bedeutet alle CSS- und JavaScript-Dateien, die im `<head>` referenziert sind, blockieren das Rendering. Für JavaScript-Dateien können wir das Verhalten mittlerweile positiv beeinflussen. Durch die Attribute `defer` oder `async` im `<script>`-Element können wir jeweils steuern, dass diese Ressource asynchron geladen und das Rendering des Browsers nicht blockiert wird. Der Unterschied der beiden Attribute ist, dass `async` geladene Dateien zwar das Rendering nicht blockieren, aber den Aufbau des DOM unterbrechen, wenn der Inhalt verarbeitet wird. Ressourcen mit dem `defer` Attribut unterbrechen den Aufbau des DOM nicht und warten, falls zwischenzeitlich fertig geladen, bis das DOM vollständig aufgebaut ist.
JavaScript asynchron laden
Erste Aufgabe bei der Optimierung ist es also, die JavaScript-Dateien zu identifizieren, die `onload`-relevant sind (z.B. Modernizr oder A/B-Testing-Scripte) – diese Dateien sollten weiterhin im `<head>` blockierend geladen werden – sowie alle weiteren, die dann asynchron geladen werden. Übrigens: Inline-JavaScript ist immer blockierend.
Critical CSS
Als nächstes geht es um CSS, das ebenfalls generell render-blockierend ist. Das ist also etwas komplexer. Für die User Experience ist asynchrones CSS eher nachteilig, der Nutzer erhält einen sogenannten FOUC (Flash Of Unstyled Content): Der Browser stellt erst ungestylte Inhalte dar und wendet anschließend das Layout an. Das ist unschön. Heißt also, wir brauchen das CSS Render-Blocking um ein wildes Hin- und Her-Springen der Seite zu vermeiden. Aber brauchen wir all das CSS, was definiert und referenziert ist? Hier fängt es an, komplexer zu werden.
Nicht jede CSS-Datei, die via `<link />` Element referenziert ist, ist auch Render-Blocking – moderne Browser entscheiden auch hier selbstständig, ob die Ressource für den aktuellen Viewport dringend benötigt wird oder etwas später in der Reihenfolge geladen werden kann. Das hängt davon ab, welche Angaben im `media`-Attribut definiert sind. So werden z.B. Print-Stylesheets, also `<link media=“print“ src=“…“ />`, zwar geladen, obwohl der User vielleicht gar nicht drucken möchte, aber nicht Render-blockierend. Ebenso werden Stylesheets, die eine Media-Query-Anweisung enthalten, die beim Laden der Seite nicht zutreffen, behandelt. Heißt `<link media=“(min-width: 1600px)“ src=“…“ />` wird auf einem kleinen Bildschirm nicht blockierend geladen.
Eine grundsätzliche Empfehlung der Web Performance Optimierung ist es, dass einzelne Dateien zusammengefasst werden sollen, damit Requests eingespart werden können. Das hat mehrere Gründe: zum einen, weil Browser nur eine bestimmte Anzahl an Requests gleichzeitig ausführen können – moderne Browser sechs bis acht Requests parallel. In der Zeit sind weitere Requests blockiert, zumindest mit einem HTTP1.x Server-Protokoll. Bei geringer Verbindungsgeschwindigkeit führen viele Requests durch die Latenzen stärker dazu, die Seite zu verlangsamen als zusammengefasste (größere) Dateien. Zudem können sich konkatenierte Dateien besser auf weitere Seitenaufrufe auswirken, da sie im besten Fall schon im Cache liegen und nicht erneut angefragt werden müssen. Der Browser muss also keine Requests an den Server machen und kann somit die Seite sehr schnell aufbauen. Das ist ein großer Vorteil.
Nun kann es aber sein, dass beim initialen Laden von CSS Stil-Anweisungen enthalten sind, die auf der Seite gar nicht zur Anwendung kommen, also unter Umständen unnötig geladen werden. Zudem gestalten wir unsere CSS- und JavaScript-Dateien aus unterschiedlichen Gründen immer modularer, so dass sie sogar gezielter geladen werden könnten. Hier gibt es also kein schwarz und weiß, hier ist es mehr ein Testen der verschiedenen Möglichkeiten, um den besten Weg für das Projekt herauszufinden, ob CSS in einer oder mehreren Dateien geladen werden soll.
Fakt ist, wir sollten überprüfen, ob es nicht noch CSS-Anweisungen gibt, die überhaupt nicht auf der Website vorkommen, und diese löschen. In den Chrome Devtools ist mittlerweile ein Werkzeug (Coverage) integriert, das beim Auffinden von unnötigen CSS sowie JavaScript hilft.
Responsive Webdesign
Der Viewport auf responsiven Webseiten oder Applikationen kann je Umfang der Webseite und Zielgruppe sehr unterschiedlich sein. Er könnte sich zudem aus verschiedenen Modulen (Dateien) zusammensetzen. Hier hat man u.U. mal zu viele, mal zu wenig Stile identifiziert. Also keine einfache Aufgabe.
Ziel ist es, nur das für den Viewport benötigte CSS inline zu laden und alles weitere asynchron (mit JavaScript) nachzuladen. Für das Erstellen des Critical CSS gibt es mittlerweile in der Webentwickler-Toolbox mehrere Möglichkeiten. Nach meiner Erfahrung funktionieren viele sehr einfach und zuverlässig.
Beispiel mit dem „Critical“ NPM Modul:
```
critical.generate({
inline: true,
base: test/,
src: index.html,
target: index-critical.html,
dimensions: [
{
height: 500,
width: 400,
},
{
height: 800,
width: 1200,
},
],
});
```
Weitere automatisierte Tools:
Zudem gibt es Tools wie PostCSS Critical Split, die es ermöglichen, genauer zu spezifizieren, welche Stile der Autor als kritisch empfindet und somit noch kleinere Dateigrößen als automatisierte Tools produzieren kann. Auch hier muss man Aufwand und Nutzen abwägen und testen.
Zurück zum kritischen Rendering-Pfad… Unser Ziel ist es, dass wir mit dem ersten Laden des HTML bereits das Critical CSS und das benötigte JavaScript inline im `<head>` laden und alle weiteren Ressourcen asynchron nachladen, so dass der First Render Paint so schnell wie möglich vom Browser ausgeführt wird.
Das Entscheidende bei dem Ganzen ist, dass das gesamte HTML mit dem Critical-CSS zusammen gzip’t nicht größer also 14kb sein darf und zudem im Head keine weiteren Render-Blocking-Scripte aufgerufen werden. Die magische Zahl ist hier 14kb, also 10 TCP Pakete – das ist die Größe, die der HTTP1.x Server beim ersten Aufruf einer Webseite schickt, bevor er den Round-Trip macht und weitere Daten sendet. Also, 14kb wäre das Optimum, ein zweiter Round-Trip wäre sicherlich auch noch ok – auch hier gilt testen, testen, testen. Ich möchte nicht unerwähnt lassen, dass Critical-CSS das i-Tüpfelchen der Performance-Optimierung sein sollte, wenn zuvor alle Aufgaben wie Requests einsparen, Bilder optimieren (Responsive Images) und und und… erledigt sind.
Optimalerweise sollten Entwickler wiederkehrenden Besuchern das Critical-CSS ersparen, da die zuvor geladene CSS-Datei im Browser-Cache liegen sollte. Serverseitig kann hier mit einem Cookie gearbeitet werden, anhand dessen entweder der Teil mit dem Critical-CSS oder der Teil mit dem Link zur gecachten CSS-Datei gerendert wird. Jeremy Keith hat das im Blog-Post Inlining critical CSS for first-time visits gut beschrieben.
Scott Jehl von der Filament Group beschreibt in seinem Blog-Post Inlining or Caching? Both Please!, wie man die neue Cache-API des Browser verwenden kann, um entweder das Critical Inline CSS oder die externe CSS-Quelle zu laden, je nachdem, ob der User die Seite zum ersten Mal oder wiederkehrend aufruft.
Scott Jehl hat auch die JavaScript Bibliothek „loadCSS“ geschrieben, die es ermöglicht, CSS via JavaScript asynchron zu laden. Dazu wird eine neue Browser-Technologie benutzt, die `<link rel=“preload“ />` Methode. Eine beispielhafte Einbindung könnte so aussehen:
```
<link rel="preload" href="path/to/mystylesheet.css" as="style" onload="this.onload=null;this.rel=stylesheet">
<noscript>
<link rel="stylesheet" href="path/to/mystylesheet.css">
</noscript>
<script>
/* loadCSS lib */
(function(){ ... }());
</script>
```
Falls JavaScript tatsächlich deaktiviert wurde, wird einfach ein `<noscript>` Block hinzugefügt mit dem Link zur eigentlichen CSS-Datei, das dann als Fallback lädt.
Resource Hints: Preload
Im obigen Beispiel wird eine neue Browser-Technik verwendet, die es Entwicklern ermöglicht, dem Browser mitzuteilen, welche Ressourcen er als kritisch einstuft und demzufolge vom Browser priorisiert asynchron geladen werden sollen. Das rel-Attribut des HTML Link-Element wurde durch einen neuen Wert erweitert. Hierdurch können nicht nur Stylesheet-Dateien vorab geladen werden, sondern auch Skripte, Bilder, Schriftdateien und viele weitere Ressourcen.
Das hilft uns zusätzlich für unser Critical Path Rendering. Nun können wir weitere Elemente unseres Pfades priorisiert behandeln. In den meisten Fällen gehören Bilder (im Sichtbereich) und Webschriften dazu. Normalerweise werden sie vom Browser niedrig priorisiert geladen. Generell werden Images-Ressourcen bereits von einem Preparser, noch bevor CSS- und JavaScript-Dateien geladen werden, erfasst und in die Ladereihenfolge einsortiert. Chrome z.B. erkennt dabei, ob ein Bild vermutlich im Sichtbereich zu sehen ist und priorisiert das vor anderen Bildern und teilweise vor JavaScript.
Mit `rel=“preload“` können wir dem Browser zudem mitteilen, welches Bild er vorab laden soll.
Ein Beispiel:
```
<link rel="preload" href="img-small.jpg" media="(max-width: 600px)" as="image">
<link rel="preload" href="img-large.jpg" media="(min-width: 601px)" as="image">
```
Wir definieren über das `as`-Attribut wie die Datei geladen werden soll, in diesem Fall `as=“image“`. Für Responsive Images hilft uns hier das `media` (Query)-Attribut des Link-Elements. Aktuell wird auch ein `srcset`-Attribut im Link-Element spezifiziert, so dass der Browser selbstständig herausfinden kann, welche passende Ressource er vorab laden soll.
Kleine Randnotiz: es ist sogar möglich, JavaScript konditional, je nach Breakpoint, vorab zu laden. Beispiel: map.js wird erst auf größeren Bildschirmen ab 600px geladen, auf kleinen Bildschirmen kommt eine statische Variante zum Einsatz.
```
<link rel="preload" as="script" href="map.js" media="(min-width: 600px)">
```
Browser-Support
Bei den neuen Browser-Techniken fragen sich Webworker immer, ob die Technik bereits eingesetzt werden kann und welche Browser das bereits unterstützen. Auf caniuse.com kann man den Browser-Support nachlesen. Aktuell unterstützen Chrome, Opera, Safari, MS Edge und die jeweiligen mobilen Browser diese Technik. Firefox hat den Support vorübergehend wieder deaktiviert, arbeitet aber mit Hochdruck an einer Weiterentwicklung. Der Internet Explorer 11 oder ältere Android-Browser unterstützen `rel=“preload“` nicht. Das ist aber nicht schlimm, da das ein Progressive Enhancement-Feature ist. Bedeutet also, wenn ein alter Browser das Feature nicht kennt, passiert auch nichts Schlimmes – das Ladeverhalten ist wie zuvor. Moderne Browser profitieren aber enorm und laden kritische Inhalte schneller.
Dennoch sollte man dabei nicht übertreiben und alles Mögliche vorab laden, sondern wirklich nur die kritischsten Inhalte. Man sollte bedenken, dass das Vorabladen auch dazu führen kann, dass das Rendering wiederum verzögert wird.
Zum Beispiel bei Schriftdateien sollte man genau überlegen, ob und welche Webfonts kritisch sind. Verändert der Webfont das Schriftbild sehr im Gegensatz zur Fallback-/Systemschrift, oder ist er nahe an einer Systemschrift? Verursacht der Webfont normalerweise ein FOUT (Flash Of Unstyled Text), der markant wahrnehmbar ist, dann ist der Webfont ein Kandidat zum Vorabladen. Lighthouse, das Audit-Tool von Google, überprüft dies mittlerweile und bezieht Webfonts in seine First Meaningful Paint (FMP) Metrik mit ein. Auch die Dateien, die den Critical Path beeinflussen, benennt Lighthouse in seiner Analyse.
Zudem sind Webfonts auch auf eine Art Render-blockierend. Wer kennt es nicht, dass man bei schlechter Verbindung zwar bereits das Layout (CSS) sieht, aber die Texte noch ausgeblendet sind, weil Webfonts noch laden. Hier ist das Browser-Verhalten in der Vergangenheit besonders negativ im iOS Safari aufgefallen, der keine Fallback-Fonts angezeigt hat, bis alle Webfonts komplett geladen waren. Dieses Verhalten gibt es zum Glück seit Safari v10 nicht mehr, und das Verhalten ist dem Verhalten der anderen Browsern angepasst – sprich: maximal drei Sekunden kein Text, wenn Webfonts noch laden. Nach drei Sekunden wird ein Fallback-Font angezeigt und im Anschluss der Webfont getauscht.
Nun könnte man meinen, dass der Browser doch bitteschön gleich einen Fallback-Font anzeigen soll, solange er den Webfont lädt. Bisher gab es aber keine Möglichkeit, dieses Browser-Verhalten zu steuern. Seit letztem Jahr funktioniert das in den meisten Browsern mit der CSS-Eigenschaft `font-display`.
```
@font-face {
font-family: WebFont;
src: url(/path/to/fonts/webfont.woff2) format(woff2),
url(/path/to/fonts/webfont.woff) format(woff);
font-weight: 400;
font-display: fallback;
}
```
Die jeweiligen Werte `block`, `swap`, `fallback`, `optional` und `auto` sind auf MDN näher beschrieben. Auch diese Eigenschaft kann problemlos als Progressive Enhancement eingesetzt werden und führt dazu, dass Font-Loading in modernen Browsern vom Autor genau definiert und verbessert werden kann. Keine Angst, wenn die Eigenschaft im IE11 (oder im MS Edge) nicht greift, denn die Microsoft-Browser zeigen seit jeher Fallback-Schriften, wenn Webfonts noch laden. Alte Android-Versionen laden Schriften wie bisher, profitieren aber leider nicht vom Performance-Boost.
Sahnehäubchen bei der Font-Ladestrategie wäre das Subsetting der jeweiligen Schriftschnitte. Hierbei können Tools wie subfont oder glyphhanger behilflich sein.
Nach der Installation des NPM CLI-Tool kann subfont ganz simpel auf der Kommandozeile benutzt werden.
```
subfont path/to/index.html -i
```
Mit dem Befehl wird der lokale HTML-Code analysiert und direkt in der Datei verändert. Zudem werden die reduzierten Schriften angelegt und automatisch in der index.html verlinkt. Subfont kann in diesem Schritt sogar Google Fonts automatisch lokal herunterladen und integrieren.
Fazit
Zusammenfassend kann man also viele kleine Verbesserungen machen, die teilweise aufwendiger umzusetzen, teilweise auch leichter zu bewerkstelligen sind, die aber u.U. viel für die User Experience bringen kann. Neue Browser-Feature wie `rel=“preload“` und `font-display` geben Entwicklern nun die Möglichkeit, das Ladeverhalten besser zu steuern und für den Nutzer schneller zu machen.
- Web Performance Optimierung: Der kritische Rendering-Pfad - 8. Februar 2019