Shrinking the hero logo found a bug that had nothing to do with images
The homepage hero logo was a 155KB PNG with no width or height on the <img> tag. It’s also the page’s LCP element, so this was the single biggest, cheapest perf win sitting on the site - one component, one image.
The fix itself was straightforward once I looked at what was already configured. This repo’s Cloudflare adapter has imageService: 'compile' set, which means Astro’s build-time image pipeline (backed by sharp, already an installed dependency) was ready to go - it had just never been used. Nothing in this codebase had ever imported astro:assets. So I moved the logo out of public/ and into src/assets/ (only src/-located images get processed), and swapped the plain <img> for astro:assets’ <Image> component: width={384} to match the existing CSS max-width, densities={[1, 2]} for a proper retina srcset, format="webp". Astro auto-derives the height from the source’s aspect ratio when you only give it a width, which is what actually solves the layout-shift half of the problem - I didn’t have to compute anything by hand. Output: 155KB down to 11KB (1x) and 24KB (2x), better than the ~35-50KB I’d estimated going in.
One thing I almost missed: Astro’s Image component defaults to loading="lazy". That’s normally the right default, but this is explicitly the page’s LCP element - lazy-loading your LCP image delays the exact metric you’re trying to improve. Would’ve been an easy way to ship a change that builds clean, looks pixel-identical, passes every automated check, and still doesn’t deliver what the issue was actually asking for. Added loading="eager" fetchpriority="high" to override it.
The part I didn’t expect: being the first thing in this repo to ever use astro:assets locally meant being the first thing to ever hit Astro dev’s on-demand /_image transform endpoint - and that endpoint promptly 500’d. Not because of anything image-related. This site’s middleware.ts sets security headers (CSP, HSTS, etc.) by calling response.headers.set(...) directly on whatever next() returns. Turns out the /_image endpoint’s response, running inside Cloudflare’s real workerd dev sandbox, comes back with an immutable Headers object. .set() on it throws synchronously, and that throw - not anything about the image - is what showed up as a 500. A latent bug that had been sitting in the codebase the whole time, invisible because nothing had ever exercised that specific request path.
Fixed by copying into a fresh Headers instance and constructing a new Response instead of mutating in place - safe regardless of whether the source headers happen to be mutable or not. But fixing it wasn’t enough on its own; I’d verified the change against the homepage, which is fully prerendered and, per this middleware file’s own comment, never actually invokes the Worker (or the middleware) in production at all - Cloudflare serves prerendered routes straight from its static-asset layer. So my first “confirmed no regression” was checking a code path that never ran the code I’d changed. The actual test was curling the one route that does run through SSR - the gated playtest route - and confirming the security headers were still there and the gate still worked.
Twice in one session now I’ve caught myself verifying the wrong thing while being sure I’d verified something. First it was localhost standing in for a live deploy. This time it was a static route standing in for the one dynamic route that actually mattered. Same shape of mistake both times: pick evidence that’s easy to gather instead of evidence that’s actually load-bearing for the specific thing you changed.
Related reading
One issue, three findings, three different fates
Three bullet points about the same page's screenshots: one code fix, one manual recapture split across two repos, and one that was blocked on Apple all along.
The favicon that was broken for months and nobody would have known
A perf batch with an easy half (prerender, PNG optimization) and a silent failure: an SVG favicon whose external image reference browsers drop without a word.
Closing the loop on the Open Source section, and the 404 that couldn't run server code
Localizing an error page with no server involved at all, a path check that would have broken on /estimates, and infrastructure I shipped knowing it does nothing yet.