
🌐 Auch auf: English · Français · Español
Mein Blog fühlte sich träge an. Die Startseite brachte rund 1,5 MB auf die Waage, und mein erster Reflex
waren die üblichen Verdächtigen: Vielleicht ist es die Datenbank, vielleicht sollte ich MariaDB
eine Major-Version hochziehen, vielleicht muss das TLS-Setup getunt werden. Klassischer Entwickler-Move
– zum Fix greifen, bevor man den Bug gefunden hat. Also habe ich diesmal zuerst das
Langweilige, Richtige gemacht: Ich habe gemessen. Das Ergebnis war eine
heilsame Ohrfeige. 📏
🩺 Schritt 1: Den echten Flaschenhals finden
Time to First Byte, drei Durchläufe von einem halbwegs kalten Client:
TTFB 0.173s | Total 0.174s | HTTP/2
TTFB 0.170s | Total 0.171s | HTTP/2
TTFB 0.185s | Total 0.187s | HTTP/20,17 Sekunden. Der Server liefert das HTML praktisch sofort aus. Das HTML selbst
ist gzip-komprimiert auf der Leitung ~18 KB groß. Datenbank, PHP, Caching-Schichten – also
all das, was ich „optimieren“ wollte – sind nicht das Problem.
Das Slow-Query-Log sah das genauso: null Einträge. Die DB kommt nie ins Schwitzen, weil
der Redis-Object-Cache die sich wiederholenden Queries abfängt, bevor sie überhaupt bei ihr ankommen.
Ein MariaDB-Major-Upgrade hätte mir exakt gar nichts gebracht. 🤷
Die 1,5 MB bestanden fast ausschließlich aus einer Sache: Bildern. Rund ein
Megabyte davon. Rätsel gelöst, bevor ich auch nur eine einzige Config-Datei angefasst hatte.
😤 Schritt 2: Die Bilder waren schon … ziemlich gut
Und jetzt kommt der nervige Teil: Die Bilder waren keine tief hängenden Früchte. Sie waren
bereits WebP (darum kümmert sich ein Kompressions-Plugin),
lazy-loaded (46 von 46 mit loading="lazy") und
wurden mit sauberem srcset/sizes ausgeliefert, sodass Vorschaubilder die kleine
Variante ziehen und nicht die riesige. HTTP/2-Multiplexing, gzip, zusammengefasstes CSS/JS –
alles schon da. Das war keine vernachlässigte Seite, sondern eine gut optimierte,
die einfach bilderlastig ist (eine Startseite voller Beitrags-Vorschaubilder).
Die verbleibenden Gewinne waren also klein und sehr konkret. Genau zwei.
🔐 Hebel 1: ECDSA statt RSA (der ehrliche TLS-Tweak)
Das Zertifikat war RSA-2048. RSA-Signaturen sind für den Server beim Handshake vergleichsweise
teuer zu berechnen, also habe ich cert-manager so umgestellt, dass er ein
ECDSA-P-256-Leaf-Zertifikat ausstellt – günstigerer Handshake, gleichwertige
(wohl sogar bessere) Sicherheit. In einem cert-manager/ingress-shim-Setup sind das drei
Annotations:
cert-manager.io/private-key-algorithm: "ECDSA"
cert-manager.io/private-key-size: "256"
cert-manager.io/private-key-rotation-policy: "Always"cert-manager hat in etwa vier Sekunden neu ausgestellt, ohne Downtime, und
openssl hat den Wechsel bestätigt: Public-Key: (256 bit) … NIST. Gebe ich ehrlich zu, dass das ein winziger Gewinn ist? Ja.
CURVE: P-256
Dank HTTP/2 passiert der Handshake einmal pro Seite und lag schon bei ~44 ms.
Aber er ist kostenlos und dauerhaft, also: mitgenommen. ✅
🖼️ Hebel 2: AVIF – wo „konvertiert“ ≠ „ausgeliefert“
AVIF schlägt WebP bei gleicher Qualität typischerweise um 20–40 %, also habe ich das
Plugin alles konvertieren lassen. Brav hat es 955 AVIF-Dateien erzeugt,
auf der Platte rund 60 % kleiner als die WebP-Originale. Super. Ich habe neu gemessen, eine
kleinere Seite erwartet und bekam …
<picture> tags: 0 | .avif references: 0 | .webp references: 220
image weight: 1066 KB (identical to before)Nichts. Die AVIF-Dateien existierten, waren sogar per HTTP erreichbar, aber die Seite
lieferte weiterhin zu 100 % WebP aus – selbst wenn ich mit
Accept: image/avif gefragt habe. Das ist die klassische „Next-Gen-Bild-Plugin
trifft nginx-Reverse-Proxy“-Falle. Die meisten dieser Plugins liefern
AVIF über Server-Rewrite-Regeln aus (Apache-.htaccess oder nginx-Snippets).
Mein WordPress läuft hinter einem nginx-Sidecar in K3s,
dessen Config von Ansible verwaltet wird – die automatisch generierten Rewrite-Regeln des Plugins
landen da nie. Also lagen die konvertierten Bilder einfach auf der Platte und bewunderten
sich selbst. 🪞
Den Page-Cache neu aufzubauen half nichts – denn der Cache war nie das
Problem, sondern die Auslieferung. Die Lösung: das Plugin von Server-Rewrite
auf die Auslieferungsmethode Picture-Tag / HTML umstellen, die
das AVIF beim Rendern einbaut:
<picture>
<source type="image/avif" srcset="….webp.avif">
<img src="….webp"> <!-- fallback for the ~5% without AVIF -->
</picture>Cache leeren, damit das statische HTML mit diesen Tags neu erzeugt wird, und plötzlich hat der
Browser die Wahl – und nimmt die kleinere Datei.
📊 Das Ergebnis
Dieselben 20 sichtbaren Bilder, Äpfel mit Äpfeln verglichen:
Before (WebP): 1038 KB
After (AVIF): 642 KB
Savings: 396 KB → 38% lighterJedes Bild geht jetzt als image/avif an AVIF-fähige Browser
(~95 %+ der Besucher); der Rest fällt über das
<img> automatisch auf WebP zurück. Kein Nachteil, ~40 % weniger Bildgewicht. 🎯
🧠 Die Erkenntnisse
- 📏 Erst messen, dann optimieren. Mein Bauch sagte „Datenbank / TLS / MariaDB-Upgrade“. Die Daten sagten „Bilder“. Der Bauch lag falsch und hätte Stunden für null Gewinn gekostet.
- 🚦 TTFB und Payload sind verschiedene Probleme. 0,17 s TTFB bei 1,5 MB Payload heißt: Finger weg vom Backend, schau dir an, was du über die Leitung schickst.
- 🖼️ „Nach AVIF konvertiert“ heißt nicht „liefert AVIF aus“. Prüf das Ende-zu-Ende – schau dir die tatsächlichen Bytes auf der Leitung mit dem richtigen
Accept-Header an, nicht nur, ob die Dateien existieren. - 🔌 Plugin-Defaults gehen von Apache aus. Auf einem nginx-Reverse-Proxy-/K3s-Setup bewirkt die „Rewrite“-Auslieferung stillschweigend gar nichts; die Picture-Tag-Methode ist dein Freund.
- ✅ Nimm die kostenlosen Gewinne trotzdem mit. ECDSA statt RSA wird dein Leben nicht verändern, aber es ist dauerhaft und kostet nichts.
Wie immer liegt der ganze Stack – die cert-manager-Config, das nginx-Setup, einfach
alles – öffentlich und versioniert in meinem Ansible-Repo:
github.com/aptupgrademe/www_k3s.
Miss doch mal deine eigene Seite. Vielleicht überrascht dich, was wirklich langsam ist. 🧑💻




