One Google Fonts link dropped my mobile PageSpeed from 100 to 83; self-hosting bought most of it back
PageSpeed Insights behavior and lab conditions change over time. These numbers are a snapshot of one static site measured on October 18, 2026 — treat them as one data point, not a specification.
On October 18, 2026, I added three web fonts to a static site and my mobile PageSpeed score fell from 100 to 83 in a single deploy. The only change was a font link. Self-hosting the same fonts as latin-subset woff2 files, with preload and font-display: swap, recovered the score to 99 the same day. The whole incident — break, diagnosis, fix — fit inside one afternoon.
The change was a pixel-parity pass on a 21-page site
I run a small static site, gillcalc.com, a 21-page aquarium calculator site hosted on Cloudflare Workers. The site used a system font stack and scored 100/100/100/100 on PageSpeed Insights mobile with an LCP of 0.8 seconds.
On October 18 I started a design pass to match a mockup pixel for pixel. The mockup tool exported CSS that referenced three typefaces — Inter for body text, Newsreader for headings, and JetBrains Mono for table headers — through a standard Google Fonts <link> tag. I wanted the design match, so I pasted the exported snippet in, deployed, and re-ran PageSpeed. The mobile score came back 83.
The gap between what I wanted and what the exported code assumed: the export was built for convenience, not for a site that treats page speed as a feature. The <link> pulls a render-blocking stylesheet from a third-party origin, and that stylesheet then schedules more cross-origin requests for the font files themselves. I fixed it by downloading the fonts, subsetting to latin, and serving them from my own origin.
Three states, measured on the same day
Every number in this table comes from PageSpeed Insights mobile runs against gillcalc.com on October 18, 2026. The only variable between states was how the fonts were delivered.
| State | PSI mobile | LCP | Speed Index | Font delivery |
|---|---|---|---|---|
| Before the design pass | 100 | 0.8s | — | System font stack, no web fonts |
Google Fonts <link> added | 83 | 3.3s | 4.6s | Render-blocking external stylesheet + cross-origin font files |
| Self-hosted woff2 + preload | 99 | 1.8–2.0s | — | Three latin-subset woff2 files, preloaded, font-display: swap |
The self-hosted payload is small because I subset aggressively: Inter at 47KB, Newsreader at 23KB, JetBrains Mono at 31KB — about 101KB of woff2 total, only the weights the design actually uses, latin characters only.
An external font link is expensive for three compounding reasons
First, the Google Fonts stylesheet is render-blocking. The browser will not paint anything until that cross-origin CSS finishes downloading and parsing, and on a throttled mobile connection that round trip is measured in seconds, not milliseconds.
Second, the font files are a second wave of cross-origin requests. The stylesheet reveals where the woff2 files live, and only then does the browser fetch them. Each hop adds DNS, TLS, and download time to the critical path.
Third, the test environment is deliberately slow. PageSpeed throttles the connection to approximate mid-range mobile hardware, which is exactly the condition where two extra cross-origin round trips turn a 0.8s LCP into a 3.3s one. On fast broadband the same page feels fine — which is why this regression ships so easily. It does not show up on the developer’s machine.
The self-hosting recipe is four steps
- Download only the weights and styles the design uses. The Google Fonts “Download family” button and the google-webfonts-helper tool both give you the raw files; skip the rest.
- Subset to the characters the site needs. For English-only sites, a latin subset cuts files to a fraction of their full size. My three latin subsets total 101KB.
- Declare the fonts in local
@font-facerules withfont-display: swap, and add<link rel="preload" as="font" type="font/woff2" crossorigin>for the one or two files that cover above-the-fold text. Preload only what the first screen needs — preloading everything duplicates work the browser would schedule anyway. - Verify with a cache-busting rerun. PageSpeed Insights caches results server-side; a repeat test can return the stale score. Appending
?cb=to the URL forces a fresh run. That is how I confirmed 83 was real and later that 99 was real.
One more check that matters: on a static host, the font files must exist in the deployed output, not only in the source tree. A missing woff2 fails silently — the page renders in the fallback font, the audit stays green, and you only notice if you watch the network tab.
The honest part: 99 is not 100
The score did not fully recover. LCP went from 0.8s on the system font stack to 1.8–2.0s on self-hosted fonts, and the overall score settled at 99 instead of 100. The preloaded woff2 files still compete with other critical resources, and font rendering still takes real work. I accepted that trade: the design calls for these typefaces, and 99 with the intended typography beats 100 with a stack I did not choose. But anyone claiming self-hosting restores your old score exactly is selling something. The free lunch is removing the cross-origin round trips, not removing the fonts.
The rule I kept afterward
This is now a standing rule across every site I run: fonts are self-hosted, third-party font links are banned by template, and the check is automated. The rule costs nothing — 101KB of static files — and the failure mode it prevents cost 17 points in one paste.
The broader lesson generalizes: any third-party <link> in the <head> is a render-blocking dependency on someone else’s origin, and every one of them deserves the same suspicion. Fonts were the expensive one on this site. The audit pass that caught it was routine; the willingness to treat a green score as a feature worth protecting was the actual fix.
This article was created with the help of AI. AI was not used to write the content; it assisted only with translation and grammar checks.