HomeWebGuides → Base64 fonts

Your stylesheet is
94% font data.

Inlining a webfont as a base64 data URI feels efficient — one request instead of two, no flash of unstyled text. It is a trap. The font is now inside a render-blocking file, it is a third larger than the binary, and nothing on the page paints until all of it arrives.

Measured on this site · August 2026 · by SPELL
94%Of our CSS was font data
+33%Base64 size penalty
3sfont-display:block hides text
141KB→2.6KBWhat we shipped after

Three costs, and only one is obvious

1. Size. Base64 encodes three bytes as four characters — a flat 33% penalty before compression, and font binaries barely compress because they are already compressed. A 75KB woff2 becomes roughly 100KB of text that gzip can do very little with.

2. Render blocking. This is the expensive one. CSS is render-blocking by definition: the browser will not paint until it has the stylesheet. Put a font inside the stylesheet and you have made the font render-blocking too — where a linked font file would have downloaded in parallel while the page painted.

3. Caching. Fonts never change. Your CSS changes constantly. Weld them together and every one-line CSS tweak re-downloads every font, forever.

What we found on our own site

Our stylesheet was 138.5KB, of which 130.2KB — 94% — was two base64 font faces. The actual CSS was 8.3KB. Every visitor waited for 130KB of font data before a single pixel appeared, and it was not even being compressed, because of a separate nginx problem.


The font-display value that hides your text

Independent of inlining, check this one line. The font-display descriptor decides what happens between the request and the font arriving.

ValueBehaviourEffect on LCP
blockText invisible up to ~3s, then swapsBad — if text is your LCP element, LCP waits
swapFallback immediately, swap when readyGood — paints at once
optionalFallback, use font only if near-instantBest for LCP, may skip the font
unsetBehaves like auto, usually ≈ blockBad by default

Ours was block. On a text-heavy page the largest contentful element usually is text, so block means your LCP is gated on a font download. swap costs you a brief flash of fallback type and buys you a paint.


The fix, in four steps

<link rel="preload"
  href="/assets/fonts/display.woff2"
  as="font" type="font/woff2" crossorigin>

Verify the fonts actually loaded rather than trusting the file sizes — decode errors are silent and the page just falls back:

await document.fonts.ready;
[...document.fonts]
  .map(f => f.family + ' ' + f.status);

A canvas measurement is stronger still: render a string in the real family and in the fallback, and compare widths. If they match, the webfont is not being used.


What it was worth here

BeforeAfter
Stylesheet, raw138.5 KB8.3 KB
Stylesheet, over the wire~141,000 bytes2,654 bytes
FontsInside the CSS, render-blocking98 KB binary, parallel, cached a year
font-displayblockswap

A 98% reduction in render-blocking bytes. The fonts still download — they are simply no longer standing between the visitor and the first paint, and they are cached for a year afterwards instead of being re-fetched with every CSS edit.

Send us a URL and we will tell you what is blocking your paint

Render-blocking assets, font strategy, compression and real Core Web Vitals — measured on your pages, with every finding attributed to the file causing it.

Book a call →

More on this