« All posts

The PageSpeed mystery: why identical pages scored differently

Why did architecturally identical static pages split into two PageSpeed score groups? An LCP=FCP clue exposes a paint race on slow hardware, fixed by inlining CSS.

A developer running 51 static tools on a single site noticed that pages scoring 90+ locally in Lighthouse split into two clean clubs on Google's real PageSpeed Insights API: some scored 90-100, others—architecturally near-identical—dropped to 70-79 with LCP spiking to 5.5 seconds. Digging into the raw API response revealed that on the slow pages, observed LCP exactly equaled observed FCP, meaning the real issue wasn't the LCP element itself but a delayed first paint.

The root cause was a race condition that only surfaces on deliberately throttled test hardware: async scripts don't block HTML parsing, but evaluating them does block the main thread, which paint also needs. On fast machines paint wins easily; on slow ones, a couple of extra JavaScript chunks were just enough to let script evaluation cut in front of paint. PSI's simulation model (Lantern) then treated this marginal timing loss as a structural dependency, inflating a sub-second delay into a dramatic 5.5-second LCP figure.

The fix was a one-line config change: inlining render-blocking CSS directly into the HTML via Next.js's experimental inlineCss flag, so paint no longer had to wait on stylesheet fetches or compete with script evaluation. Scores jumped from the 70s to the 90s the same day—until a week later the same symptom reappeared on an untouched control page, revealing that shared, load-variable test infrastructure, not the deploy, was the culprit this time.

The broader lesson for engineers: comparing observed versus simulated metrics in PageSpeed responses is the most reliable way to distinguish genuine performance regressions from noisy test infrastructure, and pairing every performance probe with a fixed schedule and an untouched control page prevents chasing ghosts.