How to preload WOFF2 fonts (without making things slower)

Preloading is one of those features where the syntax is short and the consequences are subtle. The right preload cuts your Largest Contentful Paint by 200-400ms. The wrong preload doubles your font requests and hurts more than it helps.
This post is what to actually put in your <head>, plus the gotchas that show up in real-world Lighthouse audits.
What preload does
The browser parses HTML top-down. By default, it doesn't know about a font until it parses the CSS, then parses the rules, then encounters font-family: Inter, then encounters the matching @font-face, then queues the WOFF2 download. That's a lot of waiting before the network even starts moving.
<link rel="preload"> short-circuits this:
<link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin>
The browser sees this on first HTML parse and starts the download immediately, in parallel with everything else. By the time CSS asks for Inter, the file is already in cache.
The minimum-correct preload
<link
rel="preload"
href="/fonts/inter-variable.woff2"
as="font"
type="font/woff2"
crossorigin>
Four required attributes. Miss any of them and the browser silently does something subtly wrong:
| Attribute | What happens if missing | | --- | --- | | rel="preload" | Not a preload anymore. | | as="font" | Browser doesn't know how to prioritize it. Lighthouse warns. | | type="font/woff2" | Browsers without WOFF2 support might preload anyway, then can't use it. | | crossorigin | Font fetched twice (once for preload, once for the @font-face that uses CORS). The single most common preload mistake. |
The double-fetch trap
Without crossorigin, here's what your network panel shows:
1. inter-variable.woff2, preload, 22 KB, ~150ms 2. inter-variable.woff2, @font-face request, 22 KB, ~150ms
Twice. Same file. Different CORS modes mean the browser treats them as different resources.
The fix is the four-letter word crossorigin (no value needed; the default is anonymous which is what fonts use).
Lighthouse will flag this as "Preload key requests" succeeded but won't always catch the double-fetch. Always check the network panel manually after adding preload.
When NOT to preload
Preload competes with other critical resources for bandwidth. The browser has a fixed connection budget per origin. If you preload too many things, you're robbing your CSS, JS, and images of capacity.
Don't preload:
- Below-the-fold fonts. If a weight is only used in the footer, the browser can fetch it normally after main content paints. Preloading it slows down the hero image.
- Italic / Bold variants used rarely. If 95% of your page is Regular, preload Regular only. Italic can fetch when needed.
- Multiple weights on a content site. Pick the one that paints first. Usually that's Regular for body or Bold for the headline.
- Fonts behind feature flags / A-B tests. Preload always loads, even if the experiment doesn't end up using the font.
The rule: preload exactly the fonts in your first viewport. No more.
The order of preload tags matters
Browsers respect declaration order for resource priority. Put your most-critical resource first:
<head>
<!-- Critical CSS already inline -->
<link rel="preload" href="/fonts/inter-variable.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/hero.jpg" as="image" fetchpriority="high">
<link rel="stylesheet" href="/styles.css">
<!-- Below-the-fold stuff -->
</head>
fetchpriority="high" on the hero image tells the browser the image matters more than the font (in some cases). It's a tie-breaker the browser uses when it has to choose what to download next.
Pairing preload with font-display
The maximum-leverage move is preload + font-display: optional:
<link rel="preload" href="/fonts/inter-variable.woff2" as="font" type="font/woff2" crossorigin>
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-variable.woff2') format('woff2');
font-weight: 100 900;
font-display: optional;
}
Why this combo:
font-display: optionalgives the browser ~100ms to use the font OR else fall back forever for this pageview.- Preload makes the font almost always win the 100ms race because the download started during HTML parse.
- Result: zero layout shift on warm caches, web font on every visit after the first.
This is the configuration that most consistently scores green on Core Web Vitals' CLS metric for font-heavy sites.
What about Google Fonts CSS?
If you're loading from fonts.googleapis.com, you can't easily preload because the actual font URL is inside the CSS Google returns, and that URL changes per browser/version.
Workaround:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter&display=swap">
preconnect opens a TCP+TLS connection to the font CDN early, but the browser still has to wait for the CSS to discover the actual font URL. You save ~50-100ms compared to no preconnect, but never as much as a true preload.
The real fix: self-host. Then you control the font URL and can preload it directly. See the GDPR self-hosting guide for the migration steps.
Single-line cheat sheet
For a self-hosted variable font that handles your first paint:
<link rel="preload" href="/fonts/inter-variable.woff2" as="font" type="font/woff2" crossorigin>
That's it. Pair with font-display: optional in your CSS, set a 1-year cache header, ship.
Quick recap
1. Preload exactly one font per page, the one used in your first viewport 2. Always include crossorigin (avoids the double-fetch) 3. Always include type="font/woff2" (lets non-WOFF2 browsers skip) 4. Pair preload with font-display: optional for zero CLS on warm caches 5. Don't preload below-the-fold weights, italic variants, or fonts behind A/B tests 6. Check the network panel after deploying, the preload should NOT show two requests for the same file
Self-hosted WOFF2s of common fonts (Inter, Geist, Merriweather, Playfair Display, JetBrains Mono) are pre-compressed to under 25KB each, which makes them trivial to preload without bandwidth concerns.
Frequently asked
Should I preload every font on my site?
No. Preload only the font(s) used in above-the-fold text. Each preload competes with critical resources for bandwidth. Preloading 5 weights is almost always worse than preloading 1.
What does the crossorigin attribute do on preload?
Fonts use anonymous CORS by default. Without crossorigin on the preload, the browser fetches the font twice, once via preload (no CORS) and once via @font-face (with CORS). Always include crossorigin on font preloads.
Does preloading help if I'm using Google Fonts CDN?
Marginally. The bigger win is to self-host so you skip the DNS lookup and TLS handshake for fonts.gstatic.com. Preload helps both, but self-hosting + preload is faster than CDN + preload.
Should I preload variable fonts or static cuts?
Whichever your above-the-fold text uses. If you use a variable font for everything, preload the variable. If you mix variable with statics, preload only the file the first paint depends on.