How to base64 fonts vs external font files: performance tradeoffs
- Step 1Measure your current CSS transfer size — Open DevTools Network, filter to your stylesheet, and note its compressed transfer size. Then encode one font with [font-to-base64](/font-tools/font-to-base64) and check the overhead chip — the raw base64 is +33%, but the gzipped impact depends on the source format (negligible for WOFF2, larger for TTF).
- Step 2Confirm you're on HTTP/2 or HTTP/3 — In the Network tab, add the Protocol column. If it shows `h2` or `h3`, multiple font requests share one connection with no head-of-line cost — the request-saving argument for inlining is gone. Only on `http/1.1` does inlining's request reduction matter, and almost no production CDN serves HTTP/1.1 today.
- Step 3Check whether the font is on the critical render path — If the font is only used below the fold or in a rarely-seen component, inlining it into render-blocking CSS makes every visitor pay for bytes most won't see immediately. External + `font-display: swap` lets the page paint first and the font arrive when it's ready.
- Step 4Estimate the cache cost of an edit — If you ship CSS weekly but change fonts yearly, inlining means every weekly CSS deploy re-downloads the font for returning visitors. An external content-hashed WOFF2 (`Brand.a1b2c3.woff2`) caches for a year and survives CSS edits untouched.
- Step 5Pick the strategy per environment — Web on HTTP/2+: external WOFF2 + `<link rel="preload">`. HTML email: inline base64 (external references are stripped). Offline PWA / kiosk: inline base64 so the asset is in the cached payload. Single-file demo or downloadable report: inline. The rule is — inline only when an external fetch is impossible or the subset is tiny and critical.
- Step 6If you do inline, inline the smallest critical weight only — Encode just the one above-the-fold weight, ideally a subset (use [font-subsetter](/font-tools/font-subsetter) first), and leave the rest external. That caps the render-blocking penalty while keeping the secondary weights cacheable and parallel.
Decision matrix: inline vs external
The call depends on environment, not preference. External wins on the open web; inline wins where external resources can't be fetched.
| Environment | Best choice | Why |
|---|---|---|
| Public website on HTTP/2 / HTTP/3 | External WOFF2 + preload | Parallel fetch, long-lived cache, no render-blocking inflation |
| HTML email | Inline base64 | Most clients strip external @font-face; inline is the only thing that survives (except Outlook desktop, which strips both) |
| Offline PWA / service worker | Inline base64 (in cached payload) | The font must be available with no network; inlining bakes it into the cached CSS |
| Kiosk / air-gapped app | Inline base64 | No network to fetch an external file from |
| Single-file HTML demo / downloadable report | Inline base64 | Portability — one file that renders anywhere with no dependencies |
| Tiny critical subset (logotype, hero) | Inline the subset, external the rest | A <5 KB critical subset avoids a render-path round-trip; everything else stays cacheable |
Cost dimensions, side by side
Every cost an inlined font pays that an external WOFF2 does not. Numbers are the structural properties of base64 and HTTP, not benchmarks of any one site.
| Dimension | External WOFF2 | Base64-inlined |
|---|---|---|
| Wire size | Raw binary (with gzip/brotli on the response) | +33% raw; gzip recovers it for WOFF2, but TTF base64 gzips ~17% worse |
| Request cost | One request, free on HTTP/2 multiplexing | Zero extra requests — the only genuine win |
| Render blocking | None — fetched in parallel; swap shows fallback instantly | Lives in render-blocking CSS; first paint waits on the CSS download |
| Cacheability | Content-hashed, caches for ~1 year, shared across pages | Cached only with the CSS; any CSS edit re-downloads the font |
| Cross-page reuse | Second page reuses the cached font for free | Re-sent inline in every CSS file that declares it |
| CSP exposure | font-src 'self' | Needs font-src ... data: or the font is silently blocked |
Why HTTP/2 reversed the old advice
Inlining made sense on HTTP/1.1, where each request paid a real connection cost. Modern protocols removed that.
| Protocol | Per-request cost | Verdict on inlining |
|---|---|---|
| HTTP/1.1 | 6-connection cap per origin; each font is a serialised round-trip | Inlining could genuinely save time — the historical reason data URIs caught on |
| HTTP/2 | Multiplexed over one connection; many fonts fetch in parallel | Request-saving benefit is near zero; the +33% size penalty dominates |
| HTTP/3 (QUIC) | Multiplexed with no TCP head-of-line blocking | Same as HTTP/2 — external wins by an even wider margin on lossy networks |
Cookbook
Concrete patterns for each verdict above. The encoder output is abbreviated as <BASE64>; everything else is real, copy-pasteable CSS/HTML.
Web (HTTP/2): external WOFF2 + preload — the default
ExampleThe fast path for a public site. Preload starts the font fetch in parallel with the CSS; swap paints fallback text immediately.
<!-- in <head>, before the stylesheet -->
<link rel="preload" href="/fonts/Brand.a1b2c3.woff2"
as="font" type="font/woff2" crossorigin>
/* in CSS */
@font-face {
font-family: "Brand";
font-weight: 400;
font-display: swap;
src: url("/fonts/Brand.a1b2c3.woff2") format("woff2");
}Email: inline base64 (the one place external is stripped)
ExampleWebmail and app clients strip external @font-face, so inline is the only thing that can carry the brand font. Always pair with a system fallback for Outlook desktop.
<style>
@font-face {
font-family: "Brand";
src: url("data:font/woff2;base64,<BASE64>") format("woff2");
font-display: swap;
}
</style>
<!-- use with a fallback in inline styles -->
<td style="font-family:'Brand',-apple-system,'Segoe UI',sans-serif">Hybrid: inline the critical weight, external the rest
ExampleBest of both for a perf-sensitive site that still wants a guaranteed first-paint font. Inline a tiny critical subset, link the secondary weights so they stay cacheable.
/* critical, inlined subset (~3-5 KB) */
@font-face {
font-family: "Brand";
font-weight: 400;
font-display: swap;
src: url("data:font/woff2;base64,<BASE64>") format("woff2");
}
/* secondary weights, external + cacheable */
@font-face {
font-family: "Brand";
font-weight: 700;
font-display: swap;
src: url("/fonts/Brand-Bold.woff2") format("woff2");
}The cacheability cost, made concrete
ExampleSame site, two strategies, returning visitor after a CSS-only edit. Inlining re-downloads the font; external reuses it.
Returning visitor, you shipped a 1-line CSS change: External WOFF2 (content-hashed): CSS: re-downloaded (changed) ~8 KB Font: served from cache (unchanged) 0 KB --------------------------------------- Total over the wire: ~8 KB Inlined base64 font: CSS+font: re-downloaded (CSS hash changed) --------------------------------------- Total over the wire: ~8 KB CSS + ~38 KB font
Don't forget the CSP when you inline
ExampleSwitching from external to inline silently breaks fonts if your CSP has a font-src directive without data:. This catches teams every time.
# External fonts worked under this: Content-Security-Policy: font-src 'self'; # Inlined data: URIs need this: Content-Security-Policy: font-src 'self' data:;
Edge cases and what actually happens
Every row below was probed against the live API. Some documented requirements (alphabetical axis order, numerical tuple order) are not actually enforced in practice — useful to know if you've been blaming the wrong thing for a 400.
Inlining on HTTP/2 makes the page slower, not faster
Common mistakeOn HTTP/2/3 the saved request is worth microseconds, but the +33% of base64 sits in render-blocking CSS that the browser must fully download and parse before first paint. An external WOFF2 with font-display: swap paints fallback text instantly and fetches the font in parallel. Net: external is usually faster on any modern protocol.
Every CSS edit re-downloads the inlined font
Cacheability lossAn inlined font shares the CSS file's cache key. Change one CSS rule, the file's content hash changes, and returning visitors re-download the entire base64 payload. External content-hashed fonts (Brand.a1b2c3.woff2) survive CSS edits untouched and cache for ~1 year. If your CSS churns more often than your fonts, this is a real recurring cost.
TTF base64 fights gzip; WOFF2 base64 doesn't
Format-dependentWOFF2 is already Brotli-compressed, so its base64 CSS gzips to within ~1% of the raw binary. TTF still has compressible internal structure that base64 scrambles, so base64'd TTF can gzip ~17% larger than the raw TTF would. If you inline, always inline WOFF2 — convert first with ttf-to-woff2.
Render-blocking CSS grows past your performance budget
Budget breachInlining a whole multi-weight family can push a stylesheet from ~20 KB to hundreds of KB, all render-blocking. LCP and FCP both degrade because nothing paints until that CSS arrives. Inline at most a single tiny critical weight; keep the rest external. A CI size check on the built CSS catches accidental bloat.
CSP font-src without data: silently blocks inlined fonts
Font request rejectedIf Content-Security-Policy has a font-src directive (or falls back to default-src), it must include data:. font-src 'self' allows /fonts/x.woff2 but rejects data:font/woff2;base64,... with a console warning while the CSS still parses. The font silently falls back to the system stack. Add data: to font-src.
Outlook desktop strips both inline and external @font-face
Stripped by Word engineOutlook on Windows renders through Microsoft Word's HTML engine, which removes every @font-face rule regardless of inline vs external. There's no winning strategy for Outlook desktop — the answer is a strong system-font fallback. See the HTML email guide for the Outlook-aware pattern.
Inlined fonts can't be shared across pages
No cross-page reuseAn external font cached on page A is reused for free on page B. An inlined font is re-sent inside whatever CSS each page loads. On a multi-page site this multiplies the byte cost by the number of distinct CSS bundles that declare the font — the opposite of what you want.
Source maps balloon when fonts are inlined in CSS
Tooling side effectBrowsers refuse source maps over ~10 MB, and a few inlined WOFF2 files (each ~38 KB base64) embedded in a CSS source map can push it past that ceiling, so the whole map silently fails to load. Strip data URIs from source-map inputs or build inlined-font CSS as a separate, source-map-disabled file.
HTTP/1.1 is the one case the old advice still holds
Legacy exceptionOn HTTP/1.1, each font is a serialised round-trip against a 6-connection-per-origin cap, so inlining could genuinely cut latency. That's why data-URI fonts were popular pre-2016. If you're somehow still serving HTTP/1.1 with no upgrade path, inlining a critical font may help — but upgrading the protocol helps far more.
Inlining a whole variable font defeats the point
AvoidVariable fonts already pack many weights into one file precisely to be fetched once and cached. Inlining a 100-200 KB variable font into render-blocking CSS pays the +33% penalty and the no-cross-page-cache penalty on a large asset. Keep variable fonts external and preloaded; inline only static subsets.
Frequently asked questions
Doesn't base64 save an HTTP request?
Yes, but on HTTP/2 and HTTP/3 a request is essentially free — fonts multiplex over one connection in parallel. The +33% size penalty plus the loss of independent caching far outweighs one saved request. The historical advantage existed on HTTP/1.1, where requests were expensive; modern protocols reversed it.
What about cache hits?
That's the strongest argument against inlining. An inlined font is cached only as part of the CSS file, so any CSS edit busts the font cache too. An external WOFF2 with a content-hash filename caches for ~1 year and is shared across every page — the second page visit reuses it for zero bytes.
Is there a Lighthouse / Core Web Vitals penalty?
Indirectly, yes. Inlining bloats render-blocking CSS, which delays First Contentful Paint, and blocks the parallel font fetch that helps Largest Contentful Paint. Lighthouse doesn't have a rule named 'don't inline fonts,' but both metrics measurably worsen when a large font sits in render-blocking CSS.
Does the 1.333x penalty go away after gzip?
For WOFF2, mostly — its base64 CSS gzips to within ~1% of the raw binary because WOFF2 is already entropy-coded. For uncompressed TTF/OTF, base64 fights gzip and the result can be ~17% larger than just gzipping the raw font. So if you inline, inline WOFF2, never TTF.
When is inlining actually the right call?
Four cases: HTML email (external @font-face is stripped), offline PWAs / service workers (the asset must be in the cached payload), kiosk and air-gapped apps (no network), and single-file portable HTML/reports. Plus one hybrid: inlining a tiny (<5 KB) critical subset for a logotype while keeping everything else external.
Can I inline just one weight and link the rest?
Yes, and it's the best compromise for a perf-sensitive site that still wants a guaranteed first-paint font. Encode the one critical weight (ideally subset) with font-to-base64, give it its font-weight, and declare the secondary weights as external WOFF2 so they stay cacheable and parallel.
Does inlining help privacy / GDPR?
It can, as a side effect — the font lives in your own CSS under your own domain, so no third-party font CDN sees the visitor. But you can get the same privacy with self-hosted external WOFF2 and skip the size penalty. Privacy is a reason to self-host, not specifically a reason to base64-inline.
What size of subset is small enough to inline on the web?
A practical rule is under ~5 KB after WOFF2 + base64 — roughly a Latin-only subset of one weight, or a logotype's exact glyph set. Above that, the render-blocking and cache costs outweigh the saved round-trip. Subset aggressively with font-subsetter before deciding.
Does CSS minification handle inlined fonts safely?
lightningcss (Next.js, Tailwind v4) preserves a standard, single-line base64 payload byte-for-byte. It breaks on two things: whitespace inside the payload (it drops the whole rule) and URL-safe -/_ characters (it passes them through but browsers reject them). JAD's encoder emits the safe form — keep it single-line and standard-alphabet.
Is there a hard size limit on a data URI in CSS?
Not in any browser still in development — the old 32 KB IE9 cap is long gone. The real limits are performance (parsing a multi-MB inline font blocks the main thread) and cacheability. Bundlers like Vite/Webpack warn around 4 KB, but that's a guidance threshold for their auto-inline decision, not a hard cap.
Why did everyone used to inline fonts and now they don't?
HTTP/1.1. With a 6-connection-per-origin cap and serialised requests, cutting a request genuinely helped, so data-URI assets were a recognised optimisation. HTTP/2 multiplexing (2015 onward) removed the request cost, and the +33% size penalty plus lost caching turned inlining into a net loss for most web use.
How do I generate the artefact to compare?
Encode your font with font-to-base64 — it produces the exact quoted, single-line @font-face block you'd inline, with the overhead chip showing the +33%. Then drop the equivalent external WOFF2 into a <link rel="preload"> and measure both in DevTools. The deep numbers and minifier behaviour are in base64 font edge cases and quirks.
Privacy first
Every JAD Font tool runs entirely in your browser using opentype.js and the wawoff2 WASM Brotli encoder. Your fonts never leave your device — verified by zero outbound network requests during processing.