The page that hid its own content
On a fast connection, the site opened as a gray void for seconds, then images crawled in one by one. The fixes turned out to be small. The work was proving why every slow thing existed —because each one, it turned out, was somebody's earlier fix.
The complaint was simple: first visit shows a gray page. Header and logo appear, the rest is a flat gray field for two, three, sometimes four seconds — on an office connection that speed-tests just fine. Then the hero arrives, and then the images, slowly enough that you can watch them download.
The rule I set before touching anything: no fix without a measurement that blames something specific. It's the only defense against the classic performance-work failure, where you change five things, the site feels faster, and nobody knows which change mattered or what broke.
Measuring before touching
The harness was a scripted browser run: cold cache, first visit, network and CPU throttled to a cheap phone's reality, with observers recording when the gray state appeared, when it left, and when the largest content painted. Every candidate fix would be judged by the same script, three runs, before and after.
The first run split the problem cleanly in two. The gray owned the first stretch of the timeline. The images owned the rest. Different causes, different fixes, and neither was where I expected.
The curtain
The server was rendering the full page — hero, products, text, everything — and the first thing the client did was hide it. A loading flag shipped as true inside the server-rendered state, so the app woke up believing it was mid-navigation and drew a gray curtain over content that was already on screen. The curtain lifted only after hydration finished. On a throttled phone, that's seconds of hiding finished work.
The obvious fix is one word: true becomes false. But that word had history. A teammate had deliberately set it, and nobody could remember why.
Git remembered. Someone had tried false before — and reverted it within days, with a commit message about "the reload case". The navigation guard that raised the curtain ran on every route change, including the initial load and reloads, so with the flag off, reloading any page flashed gray anyway — through a different door. The revert wasn't a mistake; it was a fix for a bug that my one-word change would have brought straight back.
So the real fix is the word plus a guard: the curtain only rises when navigating from an actual page to a different one. First loads and reloads never see it; in-app navigation keeps its loader. The measurement afterwards showed zero curtain toggles on load and reload, and the loader intact where it belongs.
The hero that rendered empty
Second finding: the server wasn't rendering the hero at all. The component fired its data fetch without awaiting it, so the server sent an empty section and the real hero appeared only after the client repeated the request. Awaiting the fetch during server render — and teaching the client to skip re-fetching data it already received in the payload — moved the hero into the very first paint. The same skip went into the translations loader, which had been quietly re-downloading dictionaries the server had already inlined.
Images encoded per request
The image half of the timeline was stranger: every image request was costing full conversion time, every time, for every visitor. The image pipeline converts originals to AVIF on the fly — good — but nothing anywhere cached the result. The CDN was configured to pass image requests straight through, and the origin re-encoded the same file for every person who asked. Two and a half seconds of CPU per image, forever.
The framework offers a one-line route cache, and it corrupted every image it touched. It JSON-serializes response bodies, which quietly mangles binary data — a 150-kilobyte AVIF came back as half a megabyte of JSON. So the cache became a small hand-written middleware: hash the URL, store the bytes and headers on disk, replay on the next request. Byte-identical output, verified by checksum, and repeat requests dropped from 2.4 seconds to under a millisecond.
Teaching the cache to say no
A cache keyed on the URL invites the internet to fill your disk. Anyone can request the same image at five thousand arbitrary sizes, and each request costs you an encode and a cache entry.
So the middleware validates before it works: only the exact modifier patterns the site actually uses, dimensions capped at retina size, sources restricted to the real image folders, and a hard ceiling on cache entries — past it, requests are served but not stored. Everything outside that shape gets a 404 before any image code runs. The legitimate site fits through; a probing script gets nothing, cheaply.
The cache that was disabled for a reason
The backend had its own curtain. Every request for interface translations ran the full dictionary query — two seconds cold — because the cache around it sat commented out. Once again: commented out deliberately, and once again the history explained it. Production runs several servers, each with its own file cache, and the old five-hour lifetime meant an editor's change appeared on one server now and on another that afternoon. Translations looked broken at random. Disabling the cache was a rational response.
The repair wasn't to argue with that reason but to shrink it: a ten-minute lifetime instead of five hours. An editor's change lands everywhere within minutes — below anyone's threshold of noticing — while the servers still answer some 99% of reads from cache. Measured cold-to-warm: two seconds down to a few hundredths, shared across all visitors, at a cost of a tenth of a millisecond on the occasional miss.
What the numbers said
Same script, same throttling, three runs each side. Unthrottled, largest-content paint went from 1.7 seconds to under half a second, with the hero present in the first frame. On the throttled phone profile — the one that matches the original complaint — the gray state went from fourteen and a half seconds to two, and largest-content paint from twenty seconds to seven. Repeat image loads went from seconds to about a millisecond, byte-identical.
Why I'm happy with it
Every slow thing on that timeline was somebody's earlier fix: the curtain guarded against a reload flash, the disabled cache guarded against vanishing translations. Deleting either without its story would have reintroduced the exact bug it was built to stop — probably weeks later, in production, with no obvious connection to my change.
So the part of this work I'd defend hardest isn't a line of code. It's the habit: measure until something specific is to blame, then read the history until the blamed thing explains itself, and only then change it — and re-run the same measurement to make the improvement a fact rather than an impression. The final diff is a handful of small changes. The page just stopped hiding what it had already done.