Preloading & Resource Hints for Web Font Delivery

This guide is part of the Font Loading & Delivery Strategies blueprint, and it addresses the single most common cause of slow text rendering: the browser discovers your critical font too late. By default a webfont is invisible to the network until the browser has downloaded the HTML, downloaded and parsed the CSS, built the CSSOM, matched a styled element to an @font-face rule, and only then requested the WOFF2 file. That chain routinely costs two to four serialized round trips before the first glyph is even requested — and if your Largest Contentful Paint element is text, every one of those round trips is added straight onto your LCP.

Resource hints break that dependency. A <link rel="preload" as="font"> tells the browser to fetch the font binary during HTML parse, in parallel with the stylesheet, so the font is already in cache by the time the CSS finishes and the paint can happen immediately. This guide defines preload, prefetch, and preconnect precisely, shows where each belongs, and gives a verification-driven workflow so you never ship a hint that the browser silently ignores.

Start your diagnosis in Chrome DevTools. Open the Network panel, filter by Font, disable cache, and reload on a throttled "Fast 4G" profile. Hover the WOFF2 request and read its Start time relative to navigation. If your critical font starts downloading hundreds of milliseconds after navigation — well after the CSS has landed — its discovery is gated behind the CSSOM, and a preload hint is the fix. Confirm the diagnosis in the Performance panel: record a trace, find the LCP marker, and check whether the font request finishes just before it.

Preload vs Prefetch vs Preconnect

These three hints are constantly confused, and using the wrong one is worse than using none. They operate at different layers of the loading pipeline.

  • preconnect warms a connection. It performs the DNS lookup, TCP handshake, and TLS negotiation for an origin ahead of time, but downloads nothing. It only helps when the font lives on a different origin than the document (a separate CDN host, or hosted Google Fonts). For same-origin fonts it is useless — the connection is already open for the HTML.
  • preload fetches a specific resource at high priority during HTML parse. This is the hint that actually pulls your critical font forward. It requires you to know the exact URL, so it only works for self-hosted (or otherwise statically-known) font files.
  • prefetch is a low-priority, speculative fetch for a resource the next navigation or a later interaction will need. It is the wrong tool for an above-the-fold font and the right tool for, say, the bold weight a comment form reveals on click. The full trade-off is covered in preload vs prefetch vs preconnect for fonts.

The mental model: preconnect opens the door, preload carries the parcel in now, prefetch leaves the parcel on the step for later. For a critical, self-hosted, above-the-fold font the answer is almost always a single preload.

Critical-path font waterfall with and without preload Two network timelines: without preload the font fetch starts after CSSOM, delaying LCP; with a preload hint the font fetch starts during HTML parse, advancing LCP. Without preload HTML parse CSSOM Font fetch LCP (late) With preload HTML parse Font fetch (preload) CSSOM LCP (early)
Preload moves the font fetch off the CSSOM critical path, advancing text paint by one to two round trips.

Baseline Configuration

Before optimising anything, get the minimum correct setup in place: a same-origin @font-face with an explicit font-display, and a single preload for the one weight your first paint needs. Everything else is tuning on top of this.

Baseline preload + @font-face

<!-- In <head>, BEFORE the render-blocking stylesheet that references the font -->
<link rel="preload" href="/fonts/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin>
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-latin-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

Three details make or break this baseline. First, crossorigin is required even for same-origin fonts, because every font fetch happens in anonymous CORS mode — omit it and the preloaded file lands in a separate cache bucket from the actual @font-face request, so the browser downloads the font twice. Second, the href must match the @font-face src URL byte-for-byte, or the preload is wasted and you get a "preloaded but not used" console warning plus a duplicate fetch. Third, type="font/woff2" lets a browser that does not support WOFF2 skip the preload rather than download a format it cannot use.

If your fonts live on a separate static-asset host rather than the document origin, add a preconnect to that host so the cross-origin connection is warm before the preload fires. Pair this with the right font-display value so that, even on the slow path, fallback text appears immediately instead of leaving a blank screen.

Step-by-Step Implementation Workflow

Each step ends with a verification check so you confirm the hint did what you intended rather than assuming it did.

Implementation Steps

  1. Identify the critical font(s). In DevTools, find the LCP element (Performance panel → LCP marker) and read its computed font-family, font-weight, and font-style. That exact face — and usually only that one — is your preload candidate. Verify: you can name the single weight/style your above-the-fold text renders in.
  2. Subset before you preload. A preload is only as cheap as the file behind it, so cut the family to the codepoints you actually serve with unicode-range subset loading and pyftsubset. Verify: the critical WOFF2 you are about to preload is under your budget (target < 50KB/subset).
  3. Emit the preload, before blocking resources. Place <link rel="preload" as="font" type="font/woff2" crossorigin> in <head> ahead of the render-blocking stylesheet, ideally right after <meta charset>. Verify: in the Network waterfall the font now starts during HTML parse, in parallel with the CSS, not after it.
  4. Add fetchpriority="high" (Chromium). Even preloads compete; mark the LCP font as the highest-priority font fetch. Verify: the Priority column reads "Highest" for that request in Chrome.
  5. Confirm no duplicate download. Reload with cache disabled and check the Font filter. Verify: the font appears once; if it appears twice, the href/src mismatch or a missing crossorigin is the cause, and there is no "preloaded but not used" warning in the console.
  6. Set immutable cache headers. Serve /fonts/* content-hashed with Cache-Control: public, max-age=31536000, immutable. Verify: a second navigation reads the font from disk cache (Size column shows "(disk cache)"), so the preload cost is paid only once.
  7. Re-measure LCP, lab and field. Run Lighthouse for a lab figure and watch real-user monitoring for the field. Verify: LCP drops (commonly 200–400ms on throttled mobile for text-LCP pages) and the "Preconnect to required origins" / render-blocking notes about fonts disappear.

Browser Compatibility & Fallback Matrix

Capability Chrome/Edge Firefox Safari Notes
rel="preload" for fonts 50+ 85+ 11.1+ Firefox shipped it late; older FF simply ignores the hint and falls back to normal discovery
rel="preconnect" 46+ 39+ 11.1+ Safe to include everywhere; ignored if unsupported
rel="prefetch" 8+ 2+ 15+ Long-supported in Chromium/Firefox; Safari support is recent
fetchpriority="high" 102+ 119+ 17.2+ Progressive enhancement — unknown attribute is ignored, falls back to default preload priority
crossorigin on font preload Required Required Required Mandatory even same-origin; omission causes a double download in all engines
WOFF2 (type="font/woff2") 36+ 39+ 10+ The type lets non-supporting engines skip the preload

Because every hint degrades to "ignored" rather than "error" when unsupported, you can include preload, preconnect, and fetchpriority unconditionally — the worst case in an old engine is the original, un-accelerated discovery path.

Code Configuration Examples

HTML preload tag (self-hosted, Chromium priority bump)

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

Preconnect for fonts on a separate asset host

<!-- Only needed when the font origin differs from the document origin. -->
<link rel="preconnect" href="https://static.example.com" crossorigin>
<link rel="preload" href="https://static.example.com/fonts/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin>

Variable-font @font-face the preload feeds

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

JS-driven loading for non-critical weights (prefetch alternative)

// Load a secondary weight after first paint without blocking the critical path.
const bold = new FontFace('Inter', 'url(/fonts/inter-700.woff2)', {
  weight: '700',
  style: 'normal'
});
bold.load().then((face) => {
  document.fonts.add(face);
  document.documentElement.classList.add('inter-bold-ready');
});

The CSS Font Loading API in the last snippet is the controlled way to bring in weights that are not needed for first paint — it keeps them off the critical path entirely instead of forcing them into a preload that would steal bandwidth from your LCP resource.

Common Pitfalls

  • Preloading every weight. Each preload competes for bandwidth at the most contended moment of the page load. Preloading three or four weights can regress LCP by starving the one resource the first paint needs. Preload the single above-the-fold weight and let the rest load normally.
  • Missing crossorigin, causing a double download. Font fetches are always anonymous-CORS, so a preload without crossorigin is cached under a different key than the real fetch. The font downloads twice and the preload buys you nothing.
  • href not matching the @font-face src. A trailing query string, a different case, or an absolute-vs-relative mismatch makes the browser treat them as two URLs. You get a "preloaded but not used" warning and a duplicate request.
  • Using preconnect for same-origin fonts. The connection to your own origin is already open from the HTML request; a same-origin preconnect does nothing but add a useless <link>. Reserve it for genuinely cross-origin font hosts.
  • Preload placed after render-blocking scripts. A <link rel="preload"> only accelerates discovery if the browser sees it early. Put it high in <head>, before blocking <script> and the main stylesheet.
  • Preloading without subsetting. Pulling a 200KB unsubsetted family forward just moves a large download earlier. Subset first so the preload carries a sub-50KB file, then accelerate it.

Frequently Asked Questions

When should I use preload versus prefetch for web fonts?

Use preload for the font your initial paint needs — it is a high-priority fetch that runs during HTML parse and lands before the CSS finishes, so it directly improves LCP on text-LCP pages. Use prefetch only for fonts a later navigation or post-interaction state will need (a bold weight revealed on click, a font for the next likely page). prefetch is a low-priority, speculative hint and is the wrong tool for anything above the fold.

Does preloading a font block the main thread?

No. preload triggers a network fetch only; it does not block HTML parsing or run any main-thread work. The single main-thread cost of a webfont — parsing the file and re-laying-out text once it is applied — happens whether or not you preload, and font-display: swap keeps text visible in a fallback while that happens. Preloading simply makes the file arrive sooner.

How do I stop preload from causing a double download?

Two things must hold: the preload href must match the @font-face src exactly, and the preload must carry crossorigin (because font fetches are anonymous-CORS, even same-origin). If either is missing the browser keeps two separate cache entries and fetches the font twice. Verify in the Network panel that the font appears exactly once with no console warning.

Should I preload hosted Google Fonts?

You generally cannot preload them usefully — the actual WOFF2 URL is hidden inside Google's CSS and is version-rotated, so there is no stable URL to name in a preload. The most you can do for the hosted path is preconnect to both fonts.googleapis.com and fonts.gstatic.com. Self-hosting removes this limitation entirely, which is one reason it is the faster default for critical text.

Related