Statische Ressourcen besser cachen

Ich habe vor einigen Wochen die Website meines Musikvereins einem Relaunch unterzogen. Nicht nur das Frontend ist erneuert, auch den kompletten technischen Unterbau habe ich neu geschrieben. So ist auch das Caching statischer Ressourcen neu, also CSS, Skripte oder Bilder. Im Folgenden beschreibe ich, was ich getan habe. Vorab: Ich nutze kein CMS oder Framework.

Schritt 1: Cachebuster

Im ersten Schritt erweitere ich den Dateinamen um eine generierte ID. Das erlaubt mir, beispiel.jpg so lange zu cachen wie ich möchte und dennoch flexibel zu sein, wenn ich die Datei einmal ersetzen möchte. Der Einfachheit halber generiere ich diese aus dem Änderungsdatum der Datei, hashe dieses und kürze es zum Schluss auf acht Zeichen runter:

function cachedFile(string $file): string
{
    return mb_substr(hash('sha384', filemtime($file)), 0, 8);
}

Was mache ich jetzt mit dieser ID? Ich könnte sie als Query Parameter anhängen: beispiel.jpg?31h123jd

Performance-Guru Steve Souders aber sagte schon 2008: Query Parameter sollte man nicht als Cachebuster verwenden. Stattdessen schreibe ich ihn direkt in den Dateinamen:

function cachedFile(string $file): string
{
    $hash = mb_substr(hash('sha384', filemtime($file)), 0, 8);
    return preg_replace('/(\.(?:css|gif|ico|jpe?g|m?js|png|svgz?|webp|webmanifest|woff2?))$/', $hash . '$1', $file);
}

Wie Ihr dem regulären Ausdruck entnehmen könnte, mache ich das nur für bestimmte Dateien:

* GIF-, JPEG-, PNG- oder WebP-Bilder
* komprimierte oder nicht komprimierte SVG-Grafiken
* CSS-Dateien
* JavaScript-Dateien und -Module
* Icons
* WOFF- und WOFF2-Schriften
* Web-App-Manifest-Dateien

Diese Einschränkung wird jetzt nochmal wichtig, denn ich muss dem Webserver anweisen, dass die mit der PHP-Funktion umbenannten Ressourcen auf ihre tatsächlichen Dateien gemappt werden. Dazu füge ich das in die Apache-Konfiguration oder in meinem Fall in die .htaccess ein:

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.+)\.([a-z0-9]+)\.(css|gif|ico|jpe?g|m?js|png|svgz?|webp|webmanifest|woff2?)$ $1.$3 [L]
</IfModule>

Diese Regel zerlegt den Dateinamen in Name, Cachebuster und Endung und fügt anschließend nur noch Name und Endung zusammen. Et voilá, ich kann nun mit cachedFile() eine Ressource mit Cachebuster referenzieren:

<img src="<?= cachedFile('beispiel.jpg') ?>" alt="" width="1600" height="900">

… wird zu:

<img src="beispiel.31h123jd.jpg" alt="" width="1600" height="900">

Schritt 2: Caching

Jetzt, wo ich einzigartige URLs für meine statischen Ressourcen habe, kann ich diese sehr stark cachen. Eigentlich möchte ich nämlich, dass die Datei nur einmal angefragt wird und danach für alle Zeit im Cache liegt. Das spart viele Requests und erhöht so die Performance meiner Seiten. Mit Cache-Control: immutable konnte Facebook 60% der Requests einsparen – und warum soll ich diese Facebook-Magie für meine kleine Vereinshomepage nicht auch nutzen?

Eigentlich ist das ja ganz simpel, ich ergänze die .htaccess um Folgendes:

<IfModule mod_headers.c>
    <FilesMatch "\.(css|gif|ico|jpe?g|m?js|png|svgz?|webp|webmanifest|woff2?)$">
        Header set Cache-Control "public, max-age=31536000, immutable"
    </FilesMatch>
</IfModule>

Denkste.

Denn das setzt diesen Cache-Header generell für alle Dateien mit diesen Dateiendungen. FilesMatch greift zudem auch nur für die tatsächlichen Dateien, nicht aber für Request-URLs. Ich hätte das aber gern nur für die mit Cachebuster versehenen, also umgeschriebenen URLs. Also setze ich in der RewriteRule eine Umgebungsvariable und im Anschluss den Cache-Control-Header nur, wenn diese Variable gesetzt ist. Es dauerte ein bisschen, ehe ich rausfand, dass solche in Rewrites erzeugten Umgebungsvariablen mit REWRITE_ gepräfixt werden, aber sei’s drum. Am Ende steht nun das in meiner .htaccess:

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.+)\.([a-z0-9]+)\.(css|gif|ico|jpe?g|m?js|png|svgz?|webp|webmanifest|woff2?)$ $1.$3 [E=IMMUTABLE,L]

    <IfModule mod_headers.c>
        Header set Cache-Control "public, max-age=31536000, immutable" env=REDIRECT_IMMUTABLE
    </IfModule>
</IfModule>

Und jetzt werden meine statischen Dateien für mindestens ein Jahr, bei gutem Browsersupport aber auf ewig gecachet. Hurra!

Schritt 3: CSS-Bilder cachen

Was mir in den letzten Tagen einfiel: Mit PostCSS kann ich Bild- und Font-URLs in meinen CSS-Dateien so umschreiben, dass sie auch einen Cachebuster enthalten. Das Tool der Wahl ist postcss-assets, das man seinen PostCSS-Plugins wie folgt hinzufügt:

postcss([
    require('postcss-assets')({
        // resolve ressources in the fonts and img directories
        loadPaths: ['fonts', 'img'],
        // add a custom cachebuster
        cachebuster: (realFilePath, resolvedFilePath) => {
            const hash = fs.statSync(realFilePath).mtime.getTime().toString(16).slice(0, 8);
            return {
                pathname: `${path.dirname(resolvedFilePath)}/${path.basename(resolvedFilePath, path.extname(resolvedFilePath))}.${hash}${path.extname(resolvedFilePath)}`
            };
        }
    }),
    // more plugins ...
]);

Auch hier mache ich mir das Änderungsdatum der Datei zu Nutze, um einen achtstelligen Hash zu erzeugen. Zum „Hashen“ verwende ich einfach .toString(16) – das ist nicht sonderlich schön und ganz bestimmt kein echter Hash, tut es aber für den Moment. Mittelfristig werde ich auch hier SHA384 verwenden, um in PHP und JavaScript den gleichen Cachebuster zu erzeugen.

Nun kann ich jedenfalls sowas in meinem CSS schreiben:

@font-face {
    src: resolve('lobster.woff2');
    font-family: 'Lobster';
}

body {
    background-image: resolve('under-construction.gif');
}

… und das wird zu:

@font-face {
    src: url('/fonts/lobster.g31mdvs2.woff2');
    font-family: 'Lobster';
}

body {
    background-image: url('/img/under-construction.sd12mjf1.gif');
}

Und wieder: Hurra! 🥳

Likes

  • Martin Schneyra
  • pie ded
  • Dean Tomasevic