A three-part series

Part 1: the tools and the architecture. Part 2: wiring up each tool. Part 3: measuring real page speed without a vendor.

The whole point of our analytics setup was to keep pages fast. So we ran our own homepage through Google's PageSpeed Insights, on its mobile setting. Desktop scored 100. Mobile scored 78.

This is the story of getting that number up, discovering how unreliable a single run is, and building something better than a single number.

Key takeaways

  • Our analytics wasn't the problem: the slow part was a font stylesheet that blocked the first paint.
  • One PageSpeed run is a lab simulation. We saw 84 and 99 on identical code. Read the median of a few runs.
  • Measure an idea before you build it. Our biggest planned change turned out not to be worth doing.
  • Real visitors' page speed beats any lab score, and you can send it to the analytics tools you already have.

What was actually slow

The report was clear once we read past the headline. On a simulated slow 4G phone, the screen stayed white for about a second and a half, because the page couldn't draw anything until our typeface's stylesheet had arrived from the font service. That stylesheet then pulled in a second file before the fonts themselves. The hero image had downloaded in well under half a second. It was waiting.

The fix kept our typeface:

1

Let the page draw first

The font stylesheet now loads in the background instead of blocking the page.

2

Show a stand-in that's the same size

Until the real font arrives, text appears in a common system font, resized to match our typeface's proportions. Lines wrap in the same places, so nothing jumps when the real font takes over.

3

Switch only when the font is fully ready

The page switches to our typeface only after it has completely downloaded, so text is never invisible while a font loads.

We also trimmed our UI library's stylesheet to the parts we actually use, from 29 KB to 11 KB compressed, generated automatically so it can't fall out of date.

Lighthouse mobile metrics for the live homepage: First Contentful Paint 0.9 s, Largest Contentful Paint 2.1 s, Total Blocking Time 100 ms, Cumulative Layout Shift 0.025, Speed Index 0.9 s, all green.
After the fixes: Lighthouse 13.5 on our live homepage, mobile settings, October 2026.

The bug only some visitors could see

Here's the lesson we least expected.

After the font change, everything passed: Lighthouse, automated tests, screenshots. Then, in a normal browser, the brand font simply never appeared. The page stayed in the stand-in font forever.

The cause was a tracker blocker. Our font service's stylesheet loads a small licence-counting file from a separate address, and tracker blockers often block that address. When that happens, the browser reports the whole stylesheet as failed, even though the fonts themselves load fine. Our new loader only waited for "success", so it never switched fonts.

The fix was small: treat "failed" the same as "loaded". The lesson is broader: lab tools never run ad blockers, but plenty of real visitors do. We now test third-party loading with parts of it deliberately blocked.

The same code scored 84 and 99

After the fixes, we ran PageSpeed Insights again. 85. Two minutes later, on the same code: 99. That evening, 84.

Lighthouse performance score of 98 for the live homepage on mobile settings, with a thumbnail of the page.
Our own run of the same test, the same evening: 98.

The reason is how the test works. It loads your page on a fast machine, records what happened, then calculates how long that would have taken on a slow phone. Small differences in that first recording, such as a server that's just restarted after a deploy or a burst of work landing a few milliseconds earlier, can move a lot of time in or out of the simulated result. Running the same version of Lighthouse that Google uses, on our own machine, five times in a row, we got 98, 100, 100, 97 and 99.

How to read PageSpeed Insights

Never trust a single run. Wait a few minutes after any deploy, run it three times, and take the middle score.

Measure before you build

The remaining slowness was JavaScript starting up. We had a plan for that: rebuild our page-building system so pages are assembled on the server, sending visitors less code. It would have taken a full working session and touched our content editor.

So we built a quick prototype first and measured it against the current site, five runs each. It saved about 15 KB of JavaScript. The scores were 95 before and 94 after: no measurable difference. What remains is the framework itself, which no rearranging of our own code can remove. We dropped the plan, and saved the session.

A better number than a score: real visitors

Lab scores answer "how fast could this be on one simulated phone?" What we really want to know is how fast it is for the people who visit. Google collects that from Chrome users, but only for sites with enough traffic, and for ours it says "No Data".

So we measure it ourselves, through the same pipeline from part 1, using the five measurements Google itself uses:

MeasurementWhat it means"Good"
LCPWhen the main content showed2.5 s or less
FCPWhen anything first showed1.8 s or less
INPThe slowest response to a tap or key during the visit200 ms or less
CLSHow much the layout jumped (a score, not a time)0.1 or less
TTFBHow long our server took to start answering0.8 s or less
  • The browser measures; we only collect. Google's open-source web-vitals library reads the browser's own measurements and sends nothing anywhere itself. It loads after the page is up, so it costs visitors nothing.
  • One event per visit, sent when the visitor leaves. Layout shift and responsiveness keep changing until then, so that's the only moment the numbers are final. Our event carries all five measurements and their ratings, and goes to all five tools like every other event.

Why not just use event timestamps?

We were asked exactly this, and it's a fair question. Three reasons it can't work. Paint times happen inside the browser's renderer, while an event's timestamp records when our script sent it, which is later. Two of the five measurements aren't times at all. And the values aren't final until the visitor leaves. You have to ask the browser.

Reading the numbers: use the 75th percentile, not the average. It's what Google uses to judge these measurements, and it reflects the experience of your slower visitors rather than hiding them. Mixpanel, Amplitude and PostHog can all chart percentiles directly. GA4 needed the five measurements registered as custom metrics first.

GA4 Custom definitions, Custom metrics tab: CLS (cls, Standard), FCP (fcp_ms, Milliseconds), INP (inp_ms, Milliseconds), LCP (lcp_ms, Milliseconds) and TTFB (ttfb_ms, Milliseconds), all event-scoped.
The five page-speed measurements registered as custom metrics in GA4.

What we'd tell a client

  • Look at what's blocking the first paint before anything else. For us it was a font, not analytics, images or code.
  • Treat PageSpeed scores as a range, and judge changes by the median of several runs.
  • Prototype and measure before a big rebuild. The cheapest change is the one you discover you don't need.
  • Test with an ad blocker on. Your lab tools won't.
  • Measure real visitors. You probably already have the analytics tool to hold the data.

That's the series. If you'd like a second pair of eyes on your own site's analytics or speed, get in touch.