Font Loading Strategies That Stay Fast

Somewhere between "the design needs Brandon Grotesque" and "the homepage takes four seconds to show text," most WordPress sites pick up two to six web font files they never think about again. Fonts are a strange performance problem: they're small compared to images, but they sit directly in the critical path of the one thing users actually came for — reading your content. A 40 KB hero image loading late is cosmetic. A 40 KB font loading late can mean a blank page.

The good news is that font loading is a solved problem. Not "solved" as in one magic setting, but solved as in: there is a short list of known techniques, each with a clear trade-off, and you can pick a sensible combination in an afternoon. This post walks through that list in order of impact.

What actually goes wrong when fonts load slowly

Browsers have to make an awkward decision when your CSS says "use this font" but the font file hasn't arrived yet. There are two failure modes, and they have delightfully ugly acronyms:

  • FOIT — Flash of Invisible Text. The browser hides text until the font arrives. Users see a laid-out page with holes where the words should be. Safari historically did this indefinitely; most browsers cap it at about three seconds. Three seconds of invisible text is an eternity.
  • FOUT — Flash of Unstyled Text. The browser shows text in a fallback font immediately, then swaps in the web font when it arrives. Users see a brief re-render — sometimes a small layout shift as letter widths change.

Here's the opinionated part: FOUT is almost always the right choice. A page that's readable in the "wrong" font is doing its job. A page that's blank in the "right" font is not. Your visitors are not typographers; they will not notice 300 milliseconds of Georgia. They will absolutely notice 3 seconds of nothing.

The layout shift caused by swapping fonts does count against your Cumulative Layout Shift (CLS) score, which is why the rest of this post exists — the goal is text that appears instantly and barely moves when the real font lands.

Step 1: Use font-display: swap (or optional)

The font-display descriptor inside @font-face tells the browser which failure mode you prefer. If you do only one thing from this article, do this.

  • swap — show fallback text immediately, swap when the font loads. The sensible default for body text and headings. Google Fonts adds &display=swap to its URLs for exactly this reason.
  • optional — show fallback text immediately; if the font isn't ready almost instantly (roughly 100 ms), skip it for this page view entirely and use it next time from cache. This is the zero-layout-shift option, and it's underrated. For sites where the brand font is a nice-to-have rather than the identity, optional is the performance-purist pick.
  • block — hide text for up to ~3 seconds. Only defensible for icon fonts, where fallback characters would render as gibberish boxes. (Better still: stop using icon fonts; inline SVG does the job without the hostage situation.)
  • fallback — a compromise: ~100 ms of invisible text, then fallback, with a short swap window. Rarely worth choosing over swap or optional.

On WordPress, where this lives depends on how fonts arrive. If your theme registers fonts through theme.json (block themes), recent WordPress versions handle font-display sensibly. If your theme enqueues a Google Fonts URL, check that it includes display=swap. If fonts come from a page builder, dig into its typography settings — most major builders added a font-display option years ago, defaulted to swap, and buried it three menus deep.

Step 2: Self-host your fonts

For years the advice was "use Google Fonts' CDN, it's probably cached from another site." That advice is dead. Browsers now partition their caches per site for privacy, so a font cached from someone else's site does nothing for yours. Every visitor downloads your fonts fresh either way — the only question is from where.

Self-hosting wins on three counts:

  1. One less connection. Fetching from fonts.gstatic.com costs a DNS lookup, TCP handshake, and TLS negotiation before a single byte of font arrives — often 100–300 ms on mobile. Fonts served from your own domain reuse the connection that's already open.
  2. Control. You choose the formats, the subsets, the caching headers, and the exact files. No third party silently changing what's served.
  3. Privacy compliance. Loading Google Fonts from Google's servers sends each visitor's IP address to Google, which raises GDPR questions in some jurisdictions. Self-hosting sidesteps the entire question.

Practically: download your fonts as WOFF2 (the format to serve to modern browsers: it is widely supported and compresses better than WOFF), upload them to your theme or media folder, and declare them with @font-face. On WordPress, several small plugins automate downloading your current Google Fonts and rewriting the references; block themes let you upload fonts directly through the Font Library in the site editor.

Step 3: Preload the one or two fonts that matter

Fonts are discovered late by default. The browser must download your CSS, parse it, build the render tree, and only then realize a font file is needed. That chain can easily push font requests a full second into the page load.

A preload hint tells the browser about the font up front:

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

Two rules keep preloading from backfiring:

  • Preload at most two files — typically your body text regular weight and maybe your heading font. Preloads compete with everything else for early bandwidth; preloading six weights just moves the traffic jam earlier.
  • Don't forget crossorigin. Font requests are always made in anonymous CORS mode, even same-origin ones. Omit the attribute and the browser downloads the font twice — once for the preload, once for real. This is the single most common preloading mistake in the wild.

Step 4: Subset ruthlessly and cut weights

A typical "full" font file carries Latin, Latin Extended, Cyrillic, Greek, Vietnamese, and hundreds of glyphs your site will never render. Subsetting — stripping the file down to the character ranges you actually use — cuts font files substantially (web.dev explains the technique). Tools like glyphhanger or the subsetting options in font conversion services make this a one-time task.

Then audit your weights. Every weight and style combination is a separate file: Regular, Italic, Medium, SemiBold, Bold, Bold Italic — that's six downloads. A useful rule of thumb: a content site needs three files (regular, italic, bold) and can often get away with two. If your design system genuinely uses five weights, consider a variable font: one file containing the entire weight axis, typically weighing about as much as two or three static weights combined. Most popular open-source families (Inter, Roboto Flex, Source Sans) ship variable versions.

Step 5: Tame the layout shift with fallback matching

Once text renders instantly in a fallback font, the remaining annoyance is the reflow when the web font swaps in — lines rewrap, the page jumps a few pixels, CLS ticks up. Modern CSS gives you override descriptors to shape the fallback font so its text occupies nearly the same space as the web font:

  • size-adjust — scales the fallback's glyphs up or down to match the web font's average character width
  • ascent-override, descent-override, line-gap-override — match the vertical metrics so line heights don't change

You define a second @font-face rule that wraps a system font (say, Arial) with these adjustments, and list it as the fallback. When the swap happens, the text barely moves. Free tools exist that compute these values for any Google Font automatically, and some WordPress performance plugins now generate the overrides for you. This is a fiddly, finishing-touch optimization — do it after everything above, and only if CLS is actually a problem in your field data.

A worked example: typical WordPress blog, before and after

BeforeAfter
Font files6 (two families × three weights, from Google CDN)2 (one variable font + italic, self-hosted, subsetted)
Total font bytes~480 KB~85 KB
Third-party connections2 (googleapis + gstatic)0
Text visible at~2.8 s (FOIT on slow mobile)immediately (swap + preload)
font-displaymissingswap, with metric-matched fallback

Numbers like these are typical, not guaranteed — but the shape of the improvement is very repeatable: fewer files, fewer bytes, zero extra connections, and text that never disappears.

What about system fonts?

The nuclear option deserves an honest mention: use no web fonts at all. The system font stack (system-ui, -apple-system, Segoe UI, Roboto, sans-serif) renders instantly, costs zero bytes, and looks native on every platform. GitHub, among others, shipped this for years. If your site's identity lives in its content, photography, or product rather than its typeface — and for most small-business sites, it honestly does — system fonts are a legitimate design decision, not a compromise. At minimum, it's the right answer for admin screens, dashboards, and anything users visit to do things rather than to soak in brand atmosphere.

The short version

  1. Set font-display: swap (or optional if you can live without the font on first visit). Never let text be invisible.
  2. Self-host as WOFF2. The shared-CDN caching benefit no longer exists, and you drop two third-party connections.
  3. Preload your one or two critical fonts — with the crossorigin attribute, or you'll download everything twice.
  4. Subset to the languages you use and cut to 2–3 files, or one variable font.
  5. If CLS still bothers you, metric-match your fallback with size-adjust and friends.

None of this requires touching your design. The typeface stays; the four-second blank stare goes. Fonts are one of the few performance problems where you genuinely can have it both ways — it just takes an afternoon of deciding to.

Photo: Ann H on Pexels