Performance: Critical CSS
#Code

Critical CSS

Wenn es um die Website-Performance geht und Entwickler die höchste Punktzahl im Page-Speed-Test erreichen möchten, trifft man schnell auf den Begriff 'Critical- CSS'. Gemeint ist damit, dass die nötigen Styles für den ersten Seitenaufbau direkt zusammen mit dem HTML und nicht wie üblich als externe Ressource geladen wird. CSS im <head> wird als “blocking request” behandelt. Das bedeutet, dass die Seite solange weiß bleibt bis das CSS vollständig geladen und verarbeitet ist. Ähnlich ist es bei JavaScript – das Verhalten kann man allerdings mit den Attributen 'async/defer' steuern. Grundsätzlich ist festzuhalten, dass alles was im Head der Seite referenziert wird, den Seitenaufbau verlangsamt. Im Umkehrschluss heißt es aber nicht, dass das gesamte JavaScript ans Ende des Quellcode verschoben werden muss, aber das ist nochmal ein anderes Thema...

Der technische Grundaufbau für Critical-CSS sieht folgendermaßen aus:

<!doctype html>
<html>
<head>
<style> /* inlined critical CSS */ </style>
<script> loadCSS('non-critical.css'); </script>
</head>
<body>
... content ...
</body>
</html>

Das erstellte Critical-CSS wird inline als <style> direkt (minifiziert) in den <head> der Seite eingebunden. Zusätzlich wird mit der JavaScript-Funktion 'loadCSS();' das restliche CSS asynchron nachgeladen und in den Browser-Cache gepackt.  

Critical-CSS erstellen

Soweit so einfach. Aber wie bekommt man das Critical-CSS extrahiert? Welches CSS ist überhaupt kritisch?

Im Responsive Webdesign ist es besonders schwer das passende Critical-CSS zu erzeugen, es gibt viele verschieden Browser-Ansichten. Würde man es perfekt machen wollen, würde das bedeuten, dass man für die verschiedenen Breakpoints unterschiedliche Critical-CSS-Dateien generieren und zudem server-seitiges Browser-Sniffing anwenden müsste, um zu wissen, welche Bildschirmauflösung der User vermutlich hat, um dann im Anschluss das passende CSS in die Seite zu integrieren. Das zieht natürlich intelligentes Caching nach sich, da wir ja schnelle „statische“ Seiten ausgeben wollen. Bei einer CMS-Seite bei der sich täglich/stündlich etwas ändert und andere Inhaltselemente im obersten Viewport sein können, müsste dies dynamisch geschehen. Auch das Apache-/Nginx-Module 'mod_pagespeed' beschreibt das Vorgehen des Moduls in dem Punkt dementsprechend – es braucht im Minimum zwei Aufrufe der Seite um das Critical-CSS heraus zu filtern und zu integrieren. Man sieht schon, das kann bei einigen Seiten sehr komplex werden.

Demzufolge müssen Webworker Aufwand und Nutzen abwägen und für eine responsive Website einen Kompromiss eingehen. Die Vorgehensweise ist nicht so eindeutig. Empfehlenswert ist es aber, die Website-Analyse zu bemühen um zu prüfen mit welcher Bildschirmauflösung die meisten Nutzer die Seite besuchen um dies als Ausgangsbasis für die Critical-Styles heran zu ziehen. Einige Tools bieten die Möglichkeit mehrere „Viewports“ für die Erstellung des Critical-CSS anzugeben um die Abdeckung der Critical-Styles zu erhöhen. Um dem Nutzer eine hohe User Experience zu bieten, sollten Webworker versuchen Critical-CSS in Webseiten zu integrieren, auch wenn es keine einfache Aufgabe ist.

Grundsätzlich gibt es zwei Wege um das Critical-CSS zu erstellen: 1. automatisch generiert 2. oder händisch annotieren.

Automatisches Erstellen von Critical CSS

Mit Hilfe von Node.JS kann man sehr einfach und sehr schnell Critical-CSS erstellen lassen. Einige Tools habe ich mal getestet.

Node.JS-basierende Tools um Critical-CSS zu erstellen

Das Ergebnis ist recht unterschiedlich. Allerdings hab ich nicht weiter untersucht, warum das so unterschiedlich ist. Ich denke, dass es einerseits am Aufbau vom CSS liegt (Sass-generiert) oder auch an was anderem.

 

Für mich zeigt sich aber das Problem des automatisiertem Erstellen ganz gut – für mein Projekt war es nicht optimal.

Manuelles Erstellen von Critical-CSS

Das manuelle Erstellen von Critical-CSS ist natürlich mit höherem Aufwand verbunden. Eine Variante ist es, dass man zwei Dateien hat, eine Datei in der man Critical-Styles schreibt und eine andere wo die unkritischen Styles rein kommen. Der bessere Ansatz, ist wenn man nur eine CSS-Datei (bzw. einzelne Sass-Module) mit /* Kommentaren */ anreichert und ein Tool die Inhalte zwischen den Kommentaren als Critical-CSS extrahiert. Solch ein Vorgehen ist z.B. auch bei css-tricks.com beschrieben. Wie gut das funktioniert, wollte ich ausprobieren. 

PostCSS Critical-Split

Die Idee von 'postcss-critical-split' ist, dass man über Kommentare wie /* critical:start */ und /* critical:end */ die Bereiche kennzeichnet, die durch den PostCSS-Prozess als Critical-CSS extrahiert werden. Dabei ist es egal, ob zwischen den Kommentaren mehrere CSS-Regeln stehen oder nur eine einzelne Eigenschaft eines Selektors. Ein Beispiel:

original.css:

/* critical:start */
.nav-main {
margin: 2em;
}

@media (min-width: 600px) {
.nav-main {
margin: 0;
}
}
/* critical:end */

a {
/* critical:start */
padding: 1em;
/* critical:end*/
transition: all 200ms ease-in;
}

a:focus {
outline: 3px solid red;
}

a:hover {
background: grey;
}

Generiertes critical.css:

.nav-main { margin: 2em; }
@media (min-width: 600px) { .nav-main { margin: 0; } }
a { padding: 1em; }

Der Code ist nur beispielhaft formatiert, er kann natürlich noch durch ein weitere PostCSS-Plugin wie CSSnano minifiziert werden.

So können Webworker fein-granular festlegen, welche Anweisungen für die erste Ansicht kritisch sind. Dabei können Link-Zustände wie ':hover' oder ':focus' ignoriert und nur die für das Layout wichtige Eigenschaften gekennzeichnet werden.

Ich finde die Variante über CSS-Kommentare gut, da es eine native CSS-Funktion ist, die benutzt wird. Es ist möglich die Keywords der Kommentare zu verändern, wenn 'critical' nicht optimal ist. Unter https://github.com/mrnocreativity/postcss-critical-split gibt es die Doku zum Plugin. Der Autor des Plugins hat seine Idee auf Medium noch näher erläutert: https://medium.com/@nocreativity/manage-your-critical-css-with-this-postcss-plugin-6be1ca226c06#.oz554if8m Ich bin mit dem Ergebnis sehr zufrieden und konnte eine deutlich kleinere Datei generieren, als die automatischen Tools.

PostCSS Critical-CSS

Ein ähnlichen Funktionsumfang bietet das PostCSS-Plugin „Critical-CSS“ (https://github.com/zgreen/postcss-critical-css)

Auch dort können bestimmte CSS-Regeln über ein Keyword als kritisch markiert werden. Bei dem Plugin wird eine eigene @-Regeln '@critical;' als Kennzeichen verwendet.


@critical;
.nav-main {
    margin: 2em;
}

@critical {
    .bar {
        border: 10px solid gold;
        color: gold;
    }
}

Zudem können auch einzelne Selektoren über 'critical-selector: this;'

 

.nav-main {
    critical-selector: this;
    margin: 2em;
}
 
@media (min-width: 600px) {
    critical-selector: this;
    margin: 0;
}

Die Syntax ist zwar CSS-nah, aber nicht CSS-konform. CSS-Linter könnten damit ein Problem haben, wenn die Ausnahmen nicht deklariert sind, deswegen finde ich den Ansatz nicht so optimal.

Grunt-Task

In dem Projekt kommt Grunt als Task-Runner zum Einsatz, sodass ein Task für das Generieren das Critical-CSS geschrieben werden musste. Nun bin ich nicht der Experte was Task-Runner betrifft, aber aus meiner Erfahrung heraus, würde ich behaupten, dass ich mit PostCSS-Plugins im Grunt-Umfeld immer wieder meine Schwierigkeiten habe. Vielleicht weil diese gesondert für Gulp geschrieben sind, vielleicht auch, weil ich es nicht richtig mache. Was aber wunderbar geklappt hat, das Node.JS-Beispiel vom Plugin-Autor als Custom-Grunt-Task einzubinden:

 


grunt.registerTask('postcss-critical', 'create critical CSS from
CSS comments', function () {
var done = this.async();
var postcss = require('postcss'),
criticalSplit = require('postcss-critical-split'),
cssnano = require('cssnano'),
autoprefixer = require('autoprefixer'),
css = '',
fs = require('fs');
function saveCssFile(filepath, cssRoot) {
fs.writeFileSync(filepath, cssRoot.css);
}

css = fs.readFileSync('./path/to/css/style.css');
postcss([
criticalSplit({'output':
criticalSplit.output_types.CRITICAL_CSS }),
autoprefixer({browsers: ['last 2 versions']}),
cssnano()
])
.process(css, {
'from': './path/to/css/style.css',
'to': './path/to/css/critical.css'
})
.then(function(result) {
saveCssFile("./path/to/critical.css", result);
console.log('critical file saved');
});
});

In dem Task wird das Critical-CSS aus den Kommentaren im CSS extrahiert, Autoprefixer ausgeführt und anschließend mit CSSnano komprimiert und als estra Datei gespeichert.

Critical Rendering Path

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 Render-Blocking-Scripte aufgerufen werden, bzw. wenn, dann auch Inline ins HTML inkludiert. 14kb ist hier die Magic-Number – das ist die Größe, die der Server beim ersten Aufruf einer Webseite schickt bevor er den Round-Trip macht und weitere Daten sendet. Wenn man es nicht unter 14kb bekommt, sollten Entwickler prüfen, ob sich der Aufwand überhaupt lohnt. 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.

Optimaler Weise sollte 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.

HTTP/2 und neue Browser-Features

Mit HTTP/2 als Server-Protokoll sind einige Web Performance Best-Practices nicht mehr notwendig. Unter anderem ist das Zusammenfassen von Dateien nicht mehr notwendig. Auch das Inlinen des Critical-CSS ist nicht mehr nötig. Durch die Erweiterung den <link> mit dem Attribut rel="preload" kann dem Browser vermittelt werden, dass die Datei 'critical.css' priorisiert geladen wird.

 

<link rel="preload" type="text/css" href="critical.css" media="all"
  as="style" onload="this.rel='stylesheet'">

Aber das hängt natürlich vom Browser-Support des Projekts ab.