
My blog felt heavy. The homepage weighed in around 1.5 MB and my first instinct
was the usual suspects: maybe it’s the database, maybe I should jump MariaDB
a major version, maybe the TLS setup needs tuning. Classic developer move
— reach for the fix before you’ve found the bug. So this time I did the
boring, correct thing first: I measured. The results were a
useful slap in the face. 📏
🩺 Step 1: Find the Actual Bottleneck
Time to first byte, three runs from a cold-ish 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 seconds. The server hands over the HTML almost instantly. The HTML itself,
gzipped on the wire, is ~18 KB. So the database, PHP, caching layers — all
the stuff I was tempted to “optimize” — are not the problem.
The slow-query log agreed: zero entries. The DB never breaks a sweat, because
Redis object-cache eats the repetitive queries before they ever reach it.
Jumping MariaDB a major version would have bought me exactly nothing. 🤷
The 1.5 MB was almost entirely one thing: images. About a
megabyte of them. Mystery solved before I’d touched a single config file.
😤 Step 2: The Images Were Already… Pretty Good
Here’s the annoying part: the images weren’t low-hanging fruit. They were
already WebP (a compression plugin handles that),
lazy-loaded (46 out of 46 loading="lazy"), and
served with proper srcset/sizes so thumbnails pull the small
variant, not the giant one. HTTP/2 multiplexing, gzip, aggregated CSS/JS —
all already in place. This wasn’t a neglected site; it was a well-optimized one
that’s just image-rich (a homepage full of post thumbnails).
So the remaining wins were small and specific. Two of them.
🔐 Lever 1: ECDSA Instead of RSA (the honest TLS tweak)
The cert was RSA-2048. RSA signatures are comparatively expensive for the server
to compute during the handshake, so I switched cert-manager to issue an
ECDSA P-256 leaf certificate — cheaper handshake, equal
(arguably better) security. In a cert-manager/ingress-shim setup it’s three
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 re-issued in about four seconds, no downtime, and
openssl confirmed the swap: Public-Key: (256 bit) … NIST. Am I being honest that this is a tiny win? Yes.
CURVE: P-256
The handshake happens once per page thanks to HTTP/2 and was already ~44 ms.
But it’s free and permanent, so: taken. ✅
🖼️ Lever 2: AVIF — Where “Converted” ≠ “Delivered”
AVIF typically beats WebP by 20–40% at the same quality, so I had the
plugin convert everything. It dutifully produced 955 AVIF files,
about 60% smaller on disk than the WebP originals. Great. I re-measured, expecting
a smaller page, and got…
<picture> tags: 0 | .avif references: 0 | .webp references: 220
image weight: 1066 KB (identical to before)Nothing. The AVIF files existed, were even reachable over HTTP, but the page was
still serving 100% WebP — even when I asked with
Accept: image/avif. This is the classic “next-gen image
plugin meets nginx reverse proxy” trap. Most of these plugins deliver
AVIF via server rewrite rules (Apache .htaccess or nginx
snippets). My WordPress runs behind an nginx sidecar in K3s
whose config is Ansible-managed — the plugin’s auto-generated rewrite rules
never make it in there. So the converted images just sat on disk, admiring
themselves. 🪞
Rebuilding the page cache didn’t help — because the cache was never the
problem, delivery was. The fix was to switch the plugin from
server-rewrite to the picture-tag / HTML delivery method, which
injects the AVIF at render time:
<picture>
<source type="image/avif" srcset="….webp.avif">
<img src="….webp"> <!-- fallback for the ~5% without AVIF -->
</picture>Purge the cache so the static HTML regenerates with those tags, and suddenly the
browser has a choice — and it picks the smaller file.
📊 The Result
Same 20 visible images, apples to apples:
Before (WebP): 1038 KB
After (AVIF): 642 KB
Savings: 396 KB → 38% lighterEvery image now ships as image/avif to AVIF-capable browsers
(~95%+ of visitors); the rest fall back to WebP automatically via the
<img>. No downside, ~40% less image weight. 🎯
🧠 The Takeaways
- 📏 Measure before you optimize. My gut said “database / TLS / MariaDB upgrade.” The data said “images.” The gut was wrong, and would have cost hours for zero gain.
- 🚦 TTFB and payload are different problems. A 0.17 s TTFB with a 1.5 MB payload means: stop touching the backend, look at what you’re shipping over the wire.
- 🖼️ “Converted to AVIF” is not “serving AVIF.” Verify end-to-end — check the actual bytes on the wire with the right
Acceptheader, not just that the files exist. - 🔌 Plugin defaults assume Apache. On an nginx reverse proxy / K3s setup, “rewrite” delivery silently does nothing; the picture-tag method is your friend.
- ✅ Take the free wins anyway. ECDSA over RSA won’t change your life, but it’s permanent and costs nothing.
As always, the whole stack — the cert-manager config, the nginx setup, all
of it — is public and version-controlled in my Ansible repo:
github.com/aptupgrademe/www_k3s.
Measure your own site sometime. You might be surprised what’s actually slow. 🧑💻




