A PageSpeed Score of 76 and Five Separate Bugs Behind It

A PageSpeed Score of 76 and Five Separate Bugs Behind It

September 19, 2026 4 min read
Build in Public MockEvalio Web Performance Frontend

Checking response headers on a live page is a boring habit. It's also how I found that /assets/*.js and /assets/*.css had no Content-Encoding header at all. The main JS bundle — about 2.38MB, with Vite already producing a ~670KB gzip artifact at build time — was being shipped completely uncompressed to every visitor, on every page. Only text/html was being compressed, because that's nginx's default gzip MIME type the moment gzip on is set, independent of gzip_types. HTML looked fine. JS and CSS silently didn't, and nothing about the setup would have surfaced that on its own.

One line of config later — explicit gzip_types for JS/CSS/JSON/SVG/fonts — that was fixed. But fixing the most obvious problem is usually just the thing that makes you look for the next one.

Checking the site's actual speed

Following that fix with a real look at page load led to App.tsx: every route except the admin pages and the mock-interview flow was eagerly imported into one bundle. That included all eight SEO landing pages built earlier the same night (that's its own story — Build #021), the blog, and every gated tool — resume rewriter, job agents, analytics, the whiteboard. A first-time visitor landing on a single marketing page downloaded code for tools they'd never open.

Converting every remaining page to React.lazy() with one Suspense boundary around turned that single ~2.38MB chunk into a shared framework runtime plus per-page chunks around 4-5KB gzip each. Heavy dependencies — docx in the resume rewriter, recharts, Monaco — now only load for someone who actually reaches the page that needs them.

Checking mobile specifically, while on the subject of speed, surfaced something unrelated to bundle size entirely: the nav bar's links used hidden md:flex with no fallback for anything narrower. On a phone, "Sign In" and "Start Free" were the only two things reachable from the header. Features, How It Works, Pricing, Blog — gone, with no way in except scrolling the homepage itself, and no way in at all from any other public page. That one wasn't a performance bug. It was a page that had been effectively unreachable on mobile for a while, found only because performance work happened to point a flashlight at the same component.

The number that explained the rest

With gzip, code-splitting, and the nav fixed, a real PageSpeed Insights mobile audit gave a Performance score of 76, and named its biggest issue directly: "Render-blocking requests — Est savings of 1,680ms," with First Contentful Paint at 3.5s and Largest Contentful Paint at 4.3s on Slow 4G.

The cause was three lines in index.css: Google Fonts loaded via @import at the top of the stylesheet. @import forces a strictly serial chain before anything can render — HTML, then the app's CSS bundle, then parse it, then discover the @import, then fetch from fonts.googleapis.com, then fetch from fonts.gstatic.com. Nothing paints until that chain finishes, and @import blocking CSSOM construction is a documented anti-pattern for exactly this reason — worse under Slow 4G, where every hop in that chain pays the connection's round-trip time again.

The fix was moving both font requests into index.html as real tags with preconnect for both hosts — visible to the browser's preload scanner, which starts fetching them in parallel with the main CSS bundle instead of waiting for it to be parsed first. Checking Resource Timing on the actual production build (vite preview, not the dev server) confirmed it: the fonts request now starts at the same instant as the CSS bundle — 87ms each — instead of only after it.

The last piece was smaller: setting a long Cache-Control on Vite's content-hashed asset filenames. Safe by construction — a hashed filename changes on every rebuild, so a stale cache can never serve stale content under a name that would still resolve.

What I don't know

No follow-up PageSpeed run is sitting anywhere after this. I know what 76 and 1,680ms looked like going in; I don't have a recorded number for what came out the other side of five fixes. That's a gap, not a result I'm going to guess at.

What I do know is that none of these five bugs were found by planning to look for them. Gzip came from checking response headers out of habit. Code-splitting came from checking speed after fixing gzip. The nav came from checking mobile while already looking at something else. The font chain came from an audit that only happened because the first three fixes had already shipped. Each fix was the reason the next problem became visible.