This site now scores 100 in every Lighthouse category on mobile, under throttling, on the emulated phone PageSpeed Insights uses. I want to write down how, partly because round numbers are satisfying and partly because almost none of the work was what I expected. The page is static text and was never slow. What the last few points bought me was an education in how speed is measured.
The honest summary is that the work came in two kinds. The first kind deleted things users actually download. The second kind explained the page to the tool. Both were necessary, only one of them is performance, and it took me a while to notice which was which.
The bytes that should not have been there
The genuinely embarrassing find was fonts. The homepage was shipping 171 KB of woff2, and 98 KB of that was two faces the homepage cannot render. Both are prose-only, both were declared in the root layout, and the comment next to one of them claimed it loaded on article pages alone. It never did. Inter was also carrying an optical sizing axis that no rule ever used, which alone was 25 KB of subset.
Moving the faces to the article route should have been the end of it, but the homepage prefetches the essays it links to, the prefetch injects the article route's stylesheet chunk, and the font preload rides along at high priority. The bytes still arrived, just 140 ms later, which is exactly the kind of fix that looks done in the HTML and is undone on the network. I only caught it by reading resource timing after load instead of the initial page source, which is what hid the problem the first time.
The prefetch now waits for hover. There was a wrong turn here too: disabling the font preload entirely kept the bytes off the homepage but cost the article pages 0.8 seconds of first paint, because without the preload hint the fetch serialises behind the stylesheet instead of running beside it. Every one of these moves was measurable in both directions, and the only reliable way through was to measure both pages after every change.
The stylesheet itself was the last render-blocking request, all 8 KB of it, so it moved into the HTML. After that the homepage is one document, one font, and nothing else standing in front of paint.
The paint that did not count
With the bytes gone, the score stuck in the high 80s, and the report made a strange claim: first paint at 0.8 seconds, largest paint at 4.1. On this page those are the same pixels. The content had been on screen for three seconds before the metric noticed it.
The cause was the entrance animation. Every block on this page fades in from opacity zero, and Chrome will not consider an element at opacity zero as a Largest Contentful Paint candidate. It also does not reconsider the element when the animation makes it visible. It waits for something else to repaint it, which here was the font swap, so LCP was being stamped by Inter arriving rather than by the text appearing.
The fix is one character in a keyframe. The animation now starts at one percent opacity, which on near-black over white is not visible, and the first paint counts. The local score went from 87 to 98 and the reported LCP fell from 4.1 seconds to 2.5 without a single byte changing.
I sat with this one for a while, because it has the shape of gaming the metric. I do not think it is. The pixels are identical before and after. The measurement was wrong about the page, and the change corrected the measurement. But it is worth being honest that nothing got faster that day, and a tool that can be off by 1.6 seconds on a static page is a tool whose numbers deserve suspicion at the margins.
The seconds that never happened
The last points were about the simulator. PageSpeed numbers are not observations. Lighthouse loads the page once, then Lantern, its simulation model, replays the trace on a modeled phone with a modeled network and prices every request and every script against that model. You are not being scored on your page. You are being scored on a pessimistic story about your page.
Two things fell out of that. The reveal stagger, which plays out in about a second on a real device, stretched to four and a half seconds of simulated Speed Index, because the model dilates the timeline roughly five times under throttling. Compressing the stagger to finish in half a second was something I should have done anyway, and the simulation is why I did it. And the framework itself sets a floor: React and the Next runtime are priced into the pre-paint story no matter how little they do, which for a while pinned the score at 96 to 98 with nothing left to fix. The floor moved when the interactive graph view left the initial bundle, where it had no business being since it loads behind a toggle, and the analytics beacons moved to after window load, where the model stops caring about them.
The result reads the way the page always felt:
| Metric | Value |
|---|---|
| First Contentful Paint | 0.9 s |
| Largest Contentful Paint | 0.9 s |
| Total Blocking Time | 40 ms |
| Cumulative Layout Shift | 0.002 |
| Speed Index | 1.1 s |
The line I care about is the first two being equal. The first paint is the largest paint. The page is done when it starts.
The other three circles
Performance is the only category with a story. Accessibility was lost and recovered over contrast: two greys that looked fine were 2.84 and 3.59 against white, both under the 4.5 the smallest text needs, and lifting them cost nothing visually. Best Practices and SEO were mostly free, which on a site like this they should be. A meta description got 25 characters shorter this week so previews stop truncating it, but that was never going to move a score.
Reflections
I wrote before that this site's one constraint is that it should be almost entirely static text. The score is downstream of that constraint. A page shaped like this ought to score 100, and the interesting part was never getting there, it was that the page did not score 100 already. Every missing point turned out to be either bytes I was shipping carelessly or the measurement misreading a page it was not built to describe.
So the advice I would actually give is to sort every proposed fix into the two kinds before doing it. Deleting downloads is performance. Explaining your page to the tool is housekeeping, sometimes necessary, occasionally clarifying, never a speedup. The score cannot tell the difference between them, which is fine, because the score was never the point. The point is that a stranger on a slow phone gets the text in under a second, and that was true before the circles turned green.