PageSpeed tooling and lab scores shift with every Chrome release. Every score in this post is a snapshot from September–early October 2026 — re-run your own measurements before acting on it.
The main bottleneck on my static tool site was not my code. It was a single 177KB third-party analytics script, and moving its load out of the parse window took five pages from 83–96 to 99–100 on Google PageSpeed Insights (mobile), with LCP dropping from 3.5s to 1.5s. This post is the full case study: the measurement traps, the numbers, the code, and the second failure I did not see coming.
Quick numbers (TallyPunch, official PSI mobile, September 2026): Page weight: mine ~14KB, third parties ~180KB — 93% was not mine Worst page: 83 → 99, LCP 3.52s → 1.52s Cost: one script-loading change. Zero changes to my own code
Measurement behavior at PSI changes over time; treat the caching quirks below as "verify against current docs," not eternal law.
If you have ever wondered why your mobile PageSpeed Insights score is so hard to move while local Lighthouse says everything is fine — the two tools are measuring different worlds. The same page, at the same moment:
| Measurement | Scores | |---|---| | Local Lighthouse (my network, mainland China) | 39 / 63 / 57 | | Official PageSpeed Insights (Google's servers) | 96 (three runs, identical) |
Local Lighthouse is the open-source audit engine running on my machine over my network. PageSpeed Insights runs Lighthouse on Google's hardware over a simulated mobile network, blended with real-user data. On my restricted network, the third-party script's download and execution timing lands randomly inside or outside the measurement window, and the local variance becomes too large to optimize against. Google's official Core Web Vitals thresholds — LCP 2.5s, INP 200ms, CLS 0.1 — are the same numbers PSI scores against, so a baseline taken anywhere else is optimizing for the wrong target.
One more trap: PSI serves cached responses for identical URL + strategy requests. Three "consistent" reads may be one cached response. Break the cache before trusting the median, and compare against an untouched control page from the same session.
My site's entire bill, from the PSI third-parties-insight audit, looked like this:
| Item | My code | Third parties | |---|---|---| | Transfer bytes | HTML + CSS + favicon ≈ 14KB | gtag.js 177KB + CF beacon 10KB | | Share of 194KB total | ~7% | 93% | | Main-thread time | ~90ms | gtag 171ms (786ms blocking, local) | | CLS | 0.000 | — |
Fourteen kilobytes of my own code, zero forced reflows, zero layout shift. Nothing to fix there — which was almost disappointing, because I had a profiler ready and no villain of my own to catch.
The diagnosis lives in three PSI audits, not in the headline score: third-parties-insight shows who eats the main thread, total-byte-weight shows who eats bytes, and long-tasks shows the causal chain — in my case a long task starting at 2429ms against an LCP of 2551ms, the task sitting exactly on top of the paint moment. That is causation, not correlation.
This also falsified, for this site shape, the common claim that TTFB is the upstream cause of LCP. My server-response-time was 1ms. On a small static site, the bottleneck is bytes and main-thread time, not the server. The TTFB chain holds or breaks depending on what kind of site you run — quantify before optimizing.
A controlled localhost A/B — same build, one variable, interleaved runs — cleanly isolated the mechanism. gtag static tag versus deferred load: TBT 343ms versus 0ms. Mechanism confirmed.
But two structural blind spots make local numbers non-portable. First, the RTT blind spot: localhost has effectively zero round-trip time, while PSI's simulation charges 150ms per request. Both local variants capped at 99 with unchanged LCP; the same page scored 83 on PSI, and the entire gap lived in the RTT chain. "No change locally" does not mean "no change on PSI." Second, my restricted network inflated the third-party cost about fivefold — 786ms of blocking locally versus 171ms in PSI's environment. Direction agreed, magnitude did not.
The rule I kept: local A/B answers "does the mechanism work," and official PSI answers "how much does it buy." Mixing the two wastes an afternoon, and I speak from experience.
Defer third-party scripts until first interaction — and pin the delay with a test
The fix keeps analytics semantics completely intact; it only moves the library's injection later:
