Signal vs Noise Web Performance Developer Experience (DX)

Stop Loading Google Fonts

Static Signal
A vast dark steampunk type foundry at night, a single self-contained brass workshop in the foreground glowing with its own amber furnace and rows of movable-type cases being assembled into a clockwork printing press, severed copper data pipelines hanging loose where they used to stretch out toward a distant cathedral-sized brass font factory receding into atmospheric purple fog, the distant factory now dim and unplugged, the local workshop's furnace humming and casting warm reflections across a reflective wet stone floor with circuit-pattern grouting glowing faintly teal and amber, cinematic shallow depth of field, moody editorial composition, no text, no people

Google Fonts is a tracker that happens to also serve fonts.

That sounds like a hot take, but it isn’t really. It’s the plain reading of what happens the moment you put https://fonts.googleapis.com in your CSS. The browser opens a connection to a Google-controlled origin, sends a request that carries the user’s IP address and Referer header, and only after that round-trip does it ever start downloading a .woff2 file. The font you wanted is the byproduct of a transaction that begins with handing Google a fresh log line for every visitor on every page.

For a while, the cost of that trade was easy to ignore. Self-hosting fonts was annoying — you had to write @font-face declarations by hand, deal with the sprawling format zoo of woff/woff2/eot/ttf, and humor Internet Explorer’s preference for EOT while Chrome wanted woff2 and Safari was weird about both. So the calculus was: hand Google a log line, get a globally-cached, well-tuned font delivery network for free. Fair.

But it’s 2026, and every part of that calculus has flipped. WOFF2 is universal. Google’s CDN is no longer faster than yours. The “free” has a regulatory cost that wasn’t visible in 2014. And self-hosting fonts has gone from “a fiddly afternoon” to “one npm install and one import line.”

This is the same anti-SaaS instinct as the one behind deleting Google Analytics — fonts.googleapis.com is just GA wearing a different costume. The fix is the same: remove the third-party request, bring the asset in-house, and let your visitors read the post without being attached to Google’s network.


The default integration most people copy out of the Google Fonts UI is something like this:

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">

In the network waterfall, that produces:

  1. DNS lookup + TLS handshake to fonts.googleapis.com.
  2. CSS request to that origin. Google’s server reads the user agent and generates a CSS file tailored to the browser — different unicode-range blocks, different format() hints, different fallback chains. This is one of the underappreciated things Google Fonts does, and it’s also exactly what makes it a tracking surface.
  3. Parse the CSS, discover the src: url(...) lines pointing at fonts.gstatic.com.
  4. DNS lookup + TLS handshake to a second origin, fonts.gstatic.com.
  5. Fetch the woff2 files themselves.

Steps 1, 2, and 4 are all round-trips you paid for before you saw a single byte of font data. On a mobile connection that’s 200-400ms of pure protocol overhead. On a cold cache, the LCP element that depends on that font won’t render until at least step 5 completes — and the browser doesn’t know about step 5 until step 2 finishes.

The preconnect hints help by parallelizing the handshakes with the rest of the page load, which is why Google tells you to include them. But preconnect is a band-aid for the fundamental shape of the problem: you have two extra origins on the critical path, when you could have zero.


The Privacy Story

In January 2022, a Munich district court ruled that a German website embedding Google Fonts via the CDN had violated GDPR by transmitting the visitor’s IP address to Google in the US without consent. The damages were €100. That sounds trivial — and it would be, except the ruling triggered a wave of automated “lawsuit letter” mills that sent shakedown demands to thousands of German site operators through 2022 and 2023. The legal posture has been contested and partially mooted in subsequent rulings, but the underlying principle hasn’t shifted: an IP address is personal data under GDPR, and sending it to a third-party processor in a foreign jurisdiction requires a lawful basis.

Google’s response was to publish a help page that says, in essence, “we don’t use the data from Google Fonts requests for advertising or profiling.” That is good. It is also not a legal answer to “did you transmit personal data to a US controller.” The DPA’s question is about the transfer itself, not the post-transfer use.

The technically clean way out is “don’t make the transfer.” Self-host the fonts and the legal question evaporates. So does the consent-banner question. So does the privacy-policy paragraph explaining why you ping Mountain View on every page load.

Even setting EU law aside, there’s a quieter point: the visitor came to read your post. They did not consent to a relationship with Google in order to do that. The default of “of course we ping Google’s servers to render the body text” is the kind of default that, once you notice it, you stop being able to defend.


The Performance Story

Self-hosted fonts are faster than Google Fonts. Not “comparable to.” Faster. This wasn’t always true, and the inversion happened gradually enough that the old wisdom — “Google’s CDN is faster than yours” — has lasted past its expiration date.

Three things flipped:

  • HTTP/2 and HTTP/3 changed the cost model. Connection setup is a much bigger fraction of total transfer time on modern protocols, and you’ve already paid that cost for your own origin. Adding two Google origins adds two new connection costs that don’t amortize.
  • The shared cross-site cache is gone. Browsers used to share font caches across sites — visiting site A would warm the cache for the same font on site B. That partitioning was killed for privacy reasons in 2020, in both Chrome and Safari. Every site now downloads its own copy regardless of where it’s hosted. So the central argument for using Google’s CDN — “the user has it cached already from another site” — just stopped being true.
  • Your own CDN is fine. If your site is on Cloudflare, Render, Vercel, Netlify, or any other modern static host, the woff2 files ship from the same edge network that ships your HTML, with HTTP/2 multiplexing over a connection the browser already opened. No second origin, no second DNS lookup, no second handshake.

I have measured this on several content sites and the typical move from Google Fonts to self-hosted shaves 100-300ms off LCP on mobile, removes two or three entries from the third-party section of Lighthouse, and bumps the Performance score by 3-8 points. Same fonts. Same look. Less waiting.


The Self-Hosting Workflow in 2026

The friction is gone. Here is the entire setup on a modern static site, using Fontsource.

npm install @fontsource-variable/inter

That installs a package containing the WOFF2 files and a CSS file with @font-face declarations for the variable Inter font, including unicode-range blocks for subsetting.

In your global CSS:

@import '@fontsource-variable/inter';

body {
  font-family: 'Inter Variable', system-ui, sans-serif;
}

That’s it. The bundler picks up the woff2 files, fingerprints them, and serves them from your own origin alongside everything else.

For a more controlled setup, skip the package import and reference the files directly. Download the woff2s once — from Fontsource’s site or google-webfonts-helper — commit them to your repo under public/fonts/, and write the @font-face rules yourself:

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-variable.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

The font-display: swap line is non-negotiable. It tells the browser to render fallback text immediately and swap in the custom font when it arrives, instead of blanking the text until the woff2 lands. This is the difference between FOUT (flash of unstyled text — fine, almost imperceptible) and FOIT (flash of invisible text — the page renders headlines as empty boxes for 200ms).

For the LCP element, preload the one font that text actually uses:

<link rel="preload" href="/fonts/inter-variable.woff2" as="font" type="font/woff2" crossorigin>

Don’t preload everything. Preload the one font your above-the-fold text uses, in the weight it needs. Preloading too many fonts is worse than preloading none — you’ll fight your own HTML for connection bandwidth.


The Variable Font Win

If you’re switching anyway, take the opportunity to switch to a variable font.

A traditional font family ships as one woff2 per weight and style: Inter-Regular.woff2, Inter-Bold.woff2, Inter-Italic.woff2, and so on. A four-weight family is four files, each 25-40KB.

A variable font ships as a single woff2 file containing the full weight axis — and often italic and width axes too. For Inter Variable, that’s one ~150KB file covering weight 100 through 900, instead of seven separate files totaling 200-300KB. One request. One cache entry. Smaller total payload. And if you ever want to use weight 532 because that’s what the designer specified, you can.

Browser support has been universal since 2018. There is no reason to ship eight static weights in 2026.

Use it through standard CSS:

h1 {
  font-family: 'Inter Variable', sans-serif;
  font-weight: 700;
}

.subtle {
  font-family: 'Inter Variable', sans-serif;
  font-weight: 350;
}

Or, for designers who want the full axis, font-variation-settings:

h1 {
  font-variation-settings: 'wght' 720, 'opsz' 24;
}

What About Bunny Fonts? FontShare?

There are drop-in privacy-respecting CDNs that serve the Google Fonts catalog without the logging. Bunny Fonts is the best-known — same URL pattern as Google, hosted in EU jurisdictions, no IP logging. FontShare is a free library of high-quality typefaces from a foundry that’s actively designing them.

Both are real options. They’re a strict improvement over Google Fonts on the privacy axis. But they’re still third-party origins on the critical path, with the same two-connection waterfall and the same dependency on someone else’s CDN being up.

If the choice is “Bunny Fonts vs Google Fonts,” pick Bunny. If the choice is “Bunny Fonts vs self-hosted,” pick self-hosted. The font weighs the same either way, and your origin is closer to the user than any third-party CDN will ever be — because the HTML is already coming from there.


How to Actually Rip It Out

I have done this on enough sites that the playbook is short.

Step 1: Inventory the fonts you actually use. Open DevTools, go to the Network tab, filter for Font, and reload the page. List the families and weights that appear. On a typical site this is two families and three or four weights. Anything else referenced in your CSS but not in that filtered list can go.

Step 2: Install the Fontsource packages. One per family. Use the @fontsource-variable/* package whenever the family has a variable version — it almost certainly does for any common typeface.

Step 3: Replace the <link> to Google Fonts with the import. Delete the preconnect and stylesheet lines from your <head>. Add the @import (or the JS-side import in a CSS-in-JS setup) for each Fontsource package.

Step 4: Add font-display: swap and one preload for the LCP font. This is the perf win. Don’t skip it.

Step 5: Re-run Lighthouse. The “Reduce third-party usage” item should drop. LCP should improve by 100-300ms on mobile. “Eliminate render-blocking resources” usually drops too. Take the screenshot.

Step 6: Remove fonts.googleapis.com and fonts.gstatic.com from your CSP. If you had a Content Security Policy listing them as allowed sources, you can remove those entries now. Smaller CSP, smaller attack surface, less to maintain.

Step 7: Update the privacy policy. Same as the GA migration — the privacy policy gets shorter, which is usually a good sign.

The whole migration is an afternoon. Less, on a small site.


The Question Behind the Question

The reason Google Fonts is the default isn’t that it’s the best font-delivery mechanism. It’s that the friction of self-hosting was, at one point, real, and Google offered a turnkey alternative at exactly the moment designers were getting tired of Arial and Verdana. The deal was: take this convenience, and in exchange you’ll quietly attach your visitors to Google’s network on every page load.

That deal made sense in 2010. It stopped making sense as soon as WOFF2 became universal, build tooling started shipping fonts as first-class npm packages, and the legal status of cross-border data transfers got complicated. We’ve been past that point for years. The default just hasn’t caught up.

This is the same shape as deleting GA, as auditing your dependency list, as migrating off WordPress. Each one is a turnkey convenience you stopped needing once your toolchain matured. Each one is a third-party request you stopped needing once you noticed how cheap it had become to handle the thing yourself.

Delete the <link>. Install Fontsource. Re-run Lighthouse. The site will be faster, the privacy policy will be shorter, and your readers will get the same beautiful Inter Variable they would have gotten anyway — just without the side order of telemetry to Mountain View.