Preload vs Prefetch vs Preconnect for Fonts

This guide is part of the Font Preloading & Resource Hints section, under the wider Font Loading & Delivery Strategies area. The four hints — preload, prefetch, preconnect, dns-prefetch — look interchangeable and are constantly misused. Applied to the wrong font at the wrong priority they either do nothing or actively delay your LCP. This page maps each hint to the exact font scenario it was built for, with a decision matrix, several fully worked configurations, and the two mistakes that cause a silent double-fetch.

Problem Statement

<link rel="preload"> fetches a resource the current page will definitely need, at high priority, before the parser discovers it in CSS. <link rel="prefetch"> fetches a resource a future navigation will probably need, at the lowest priority, when the browser is idle. <link rel="preconnect"> opens the TCP + TLS connection to an origin early but downloads nothing. <link rel="dns-prefetch"> resolves only the DNS for an origin — a cheap, wider-supported fallback for preconnect. Engineers reach for preload reflexively, preloading every weight and every third-party font, which saturates bandwidth on the connection and pushes the real LCP resource back in the priority queue. The fix is matching the hint to the font's role: critical-now, next-page, or cross-origin — and knowing exactly which attributes make each hint actually take effect rather than silently no-op.

Choosing a font resource hint A four-step decision process for picking preload, preconnect, dns-prefetch, or prefetch for a given font. Choosing a font resource hint 1 Paints LCP now? self-hosted 2 Yes: preload + crossorigin 3 Third-party origin? preconnect 4 Next page only? prefetch, idle
Match the font's role to the hint built for it.

Prerequisites

  • Fonts served as WOFF2 with long-lived immutable Cache-Control (see Cache-Control: immutable for long-lived font files) so prefetched and preloaded assets survive into the navigation that uses them.
  • For self-hosted fonts, you know which single weight paints the above-the-fold LCP text — usually the body copy weight, not a display or black weight used only in a hero.
  • For third-party fonts (Google Fonts, Adobe Fonts, a font CDN), you have the connecting origin(s) — e.g. fonts.googleapis.com (CSS) and fonts.gstatic.com (font files) for Google Fonts.
  • Every @font-face uses font-display: swap (see font-display values explained) so a missed or mistimed hint degrades to a visible fallback swap rather than hiding text behind FOIT.
  • You understand how the browser decides format support, since a preload with the wrong type attribute is silently skipped by browsers that do not support that format — see choosing WOFF2 vs WOFF vs TTF.

Implementation: The Correct Hints

Put the critical-path hints first in <head>, before the stylesheet that references the fonts, so the preload scanner acts before CSSOM construction. The preload scanner is a lightweight pre-parser that runs ahead of the main HTML parser specifically to discover resources like this early — hints placed after the stylesheet lose most of their benefit because the browser would have discovered the font via CSS at nearly the same time anyway.

Correct font resource hints in <head>

<head>
  <!-- Self-hosted critical weight: fetch now, high priority -->
  <link rel="preload" href="/fonts/inter-regular.woff2"
        as="font" type="font/woff2" crossorigin>

  <!-- Third-party font origin: open the connection early, download nothing -->
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link rel="dns-prefetch" href="https://fonts.gstatic.com">

  <!-- Stylesheet that declares @font-face comes after the hints -->
  <link rel="stylesheet" href="/css/main.css">
</head>

Three load-bearing details. First, as="font" is mandatory — without it the browser cannot set the right priority or apply the correct Accept headers, and DevTools may warn that the preload went unused even though the font eventually loaded. Second, crossorigin is required even for same-origin fonts: font fetches always use anonymous CORS mode per the Fetch specification, so a preload without crossorigin lands in a different cache partition than the @font-face request and the browser fetches the file twice — once for the preload, once for the real CSS-triggered request. Third, preconnect to a font file origin (fonts.gstatic.com) also needs crossorigin, because the font fetch it warms up is itself anonymous-CORS; a preconnect without it opens a connection that the real request cannot reuse, so the browser opens a second connection anyway and the hint accomplishes nothing but wasted DNS/TLS work.

For the next-navigation case — say you know the checkout page uses a display weight the current page does not render — use prefetch, which fires at idle priority and will not compete with the current page's LCP candidate.

Anti-pattern fixes

<!-- WRONG: preload missing crossorigin -> double fetch -->
<link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2">
<!-- FIX: add crossorigin -->
<link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin>

<!-- WRONG: preloading a next-page-only font on this page -->
<link rel="preload" href="/fonts/display-black.woff2" as="font" type="font/woff2" crossorigin>
<!-- FIX: it's for the *next* navigation -> prefetch (idle priority) -->
<link rel="prefetch" href="/fonts/display-black.woff2" as="font" crossorigin>

<!-- WRONG: preconnect to a third-party font origin without crossorigin -->
<link rel="preconnect" href="https://fonts.gstatic.com">
<!-- FIX: font fetches are anonymous-CORS, so the warmed connection must match -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

The double-fetch trap is the single most common font-hint bug. A preloaded font that is fetched again by the CSS engine doubles your transfer and wastes the high-priority slot the preload occupied. Verify there is exactly one network row per font URL. Preload only above-the-fold weights — one or two files — and let the rest load on discovery. For deeper LCP-safe preloading, see preloading critical fonts without blocking LCP, and for finer-grained control over the preload's queue position see using fetchpriority to prioritise font requests.

Worked Example: Two Weights Plus a Third-Party Fallback

A common real layout: a self-hosted body weight paints the LCP paragraph, a self-hosted bold weight paints an above-the-fold heading, and a Google Fonts monospace face is used only for a code sample lower on the page.

<head>
  <!-- LCP text: body weight, preload -->
  <link rel="preload" href="/fonts/inter-400.woff2" as="font" type="font/woff2" crossorigin>
  <!-- Above-the-fold heading: bold weight, preload -->
  <link rel="preload" href="/fonts/inter-700.woff2" as="font" type="font/woff2" crossorigin>
  <!-- Below-the-fold monospace from Google Fonts: warm the connection only -->
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link rel="dns-prefetch" href="https://fonts.gstatic.com">
  <link rel="stylesheet" href="/css/main.css">
</head>

Notice that the monospace face gets no preload at all — it is not needed for the first paint, so preloading it would compete with the two weights that actually gate LCP. The preconnect still helps: when the browser eventually discovers the @font-face rule for the monospace face further down the CSS, the TCP/TLS handshake to fonts.gstatic.com is already warm, shaving 100–300ms off that later fetch depending on round-trip time. This is the general rule: preload what paints now, preconnect to what you will need soon but cannot name yet, prefetch what the next page needs.

Worked Example: Prefetching Across a Known Funnel

If your analytics show that 70%+ of visitors on a product page proceed to checkout, and checkout renders a distinct serif display face the product page never uses, prefetch it from the product page:

<!-- On the product page: warm the checkout-only display face -->
<link rel="prefetch" href="/fonts/display-serif-600.woff2" as="font" type="font/woff2" crossorigin>

Because prefetch runs at the browser's lowest priority and only during idle network time, it never delays the product page's own LCP. If the user never navigates to checkout, the wasted download is one font file — acceptable given the conversion rate. If they do navigate, the font is already in the HTTP cache and paints with font-display: swap's first paint essentially instant, since no network round trip is needed. Do not use prefetch for the current page's fonts — that is what preload is for — and do not use it speculatively for pages the user has a low chance of visiting, since idle-priority fetches still consume real bandwidth on constrained connections.

Priority: preload vs prefetch Bar chart comparing network priority of a preloaded body weight, a preloaded bold weight, and a prefetched checkout-only display face. Priority: preload vs prefetch Body weight High Bold heading High Checkout face Lowest relative priority
Preload runs at High priority; prefetch waits for idle time.

Decision Matrix

Hint What it does Priority Use for fonts when… crossorigin?
preload Fetches the file now, before discovery High The weight paints above-the-fold text on this page Required
preconnect Opens TCP + TLS, no download n/a The font lives on a third-party origin (fonts.gstatic.com) Required
dns-prefetch Resolves DNS only n/a Fallback for preconnect, or many cross-origins No
prefetch Fetches at idle, for a future page Lowest A weight only the next navigation needs Recommended

Use at most one or two preload hints; pair every third-party preconnect with a dns-prefetch fallback; reserve prefetch for assets the current page does not render.

Edge Cases and Browser Quirks

Connection limits on preconnect. Browsers cap the number of preconnected origins they will actually open sockets for — typically around four to six, depending on engine and available memory. If a page preconnects to six third-party origins for fonts, analytics, and ads combined, some of those preconnects are silently ignored. Reserve preconnect for origins that matter for render-blocking or LCP-adjacent resources, and let dns-prefetch cover the rest.

Safari's historically weaker prefetch support. Safari has, in various versions, treated prefetch more conservatively than Chromium or Firefox, sometimes deferring it further or applying stricter idle-time heuristics. Do not assume a prefetched font is guaranteed to be cached by the time the next page loads in every browser — treat it as a probabilistic optimization, not a guarantee, and keep font-display: swap on the destination page regardless.

Preload without a matching @font-face. If you preload /fonts/inter-700.woff2 but the CSS never actually declares an @font-face that resolves to that exact URL (a typo, a build hash mismatch, or a CSS file that failed to deploy), DevTools logs "the resource was preloaded using link preload but not used within a few seconds." This is not just a warning to ignore — it means you spent a high-priority network slot on nothing, at the direct expense of your real LCP resource.

Preloading a variable font with unicode-range. If your @font-face block splits a variable font into several unicode-range slices, a single preload cannot target "the one you need" ahead of knowing which characters render. Preload the slice that covers Latin/basic text (the one nearly every page uses) and let the browser discover the others through normal CSS parsing — see subsetting variable fonts by axis for how those slices are built.

HTTP/2 and HTTP/3 multiplexing does not remove the need for hints. Multiplexed connections let many requests share one socket, but they do not change discovery time or priority. A font the parser has not discovered yet is not requested yet regardless of protocol version — preload still buys real milliseconds by moving discovery earlier, and fetchpriority="high" on top of preload can further protect the slot against contention with other high-priority resources like the LCP image.

Preloading fonts loaded through the CSS Font Loading API. If fonts are loaded imperatively via new FontFace(...).load() instead of a @font-face block discovered by the CSS parser (see the CSS Font Loading API implementation guide), a preload hint still warms the network fetch — the browser matches the preloaded response by URL regardless of what triggers the eventual request — but you must ensure the crossorigin mode used by FontFace.load() matches the preload's, or the same cache-partition mismatch occurs.

Preconnect origin budget Meter showing 2 of roughly 5 available preconnect slots used by font origins on this page. Preconnect origin budget 0 ~5 origins 2 origins preconnected ~5 slot cap
Browsers only honor a handful of preconnects; extras are dropped.

Verification

Confirm each hint behaves as intended in the Network panel.

  1. DevTools → Network → Filter: Font. Reload with cache disabled.
  2. Read the Priority column. A preloaded font should show High; a prefetched font should show Lowest. If your "preload" shows Low, the as="font" attribute is missing.
  3. Check the Initiator column: the preloaded font's initiator should be the <link> element, not stylesheet. A stylesheet initiator means the preload was ignored or mismatched — usually a URL that does not exactly match what @font-face resolves to.
  4. Confirm exactly one network row per font URL. Two rows for the same URL is the double-fetch signature — almost always a missing crossorigin.
  5. For preconnect, open the font request's Timing tab; the "Initial connection" and "SSL" segments should be near-zero because the connection was already warm. If they are not, the preconnect lacked crossorigin and the warmed socket went unused.
  6. For prefetch, navigate away and back, or check the Size column on the destination page's font request — it should read (disk cache) or (memory cache) rather than a byte count, confirming the prefetch actually populated the cache before the navigation happened.
  7. Cross-check with Lighthouse's "Preload key requests" audit, which flags both missing preloads on critical chains and preloads that went unused — see Lighthouse font audits in CI for wiring this into a pipeline so a regression fails the build automatically.

Common Pitfalls

  • Omitting crossorigin on a font preload. Font requests are anonymous-CORS; the preloaded response lands in a different cache partition and the CSS engine fetches the file again. Always add crossorigin, even same-origin.
  • preconnect without crossorigin to a font-file origin. The warmed connection does not match the anonymous-CORS font fetch, so the browser opens a second connection and the hint is wasted.
  • Using preload for a next-page font. It steals a high-priority slot from the current LCP. Use prefetch, which runs at idle and waits for the navigation.
  • Preloading too many weights. Three or more font preloads saturate the connection and delay the real LCP candidate. Limit to the one or two above-the-fold weights.
  • Missing as="font". Without it the browser cannot prioritize correctly, may apply the wrong Accept header, and often warns the preload was unused.
  • Preconnecting to more origins than the browser will honor. Beyond roughly four to six simultaneous preconnects, extras are dropped; prioritize the font CDN over lower-value origins.
  • Preloading a URL that does not exactly match the @font-face src. A hashed filename mismatch between the preload href and the CSS src means the preload is wasted and the real request starts from zero — always generate both from the same build-time manifest.

Frequently Asked Questions

Why does a font I preloaded get fetched twice? The preload <link> lacks crossorigin. Fonts are always requested in anonymous-CORS mode, so a preload without crossorigin is cached separately from the @font-face request; the browser cannot reuse it and fetches the file a second time. Add crossorigin to the preload — and to any preconnect aimed at a font-file origin.

Should I preconnect or preload third-party fonts like Google Fonts? Preconnect to the origins (fonts.googleapis.com for the CSS and fonts.gstatic.com for the files, both with crossorigin), because you usually do not know the exact hashed font URL ahead of time. Preconnect warms the connection so the font request skips DNS, TCP, and TLS once the CSS reveals the URL. Preload only when you control and know the final font URL — which is one reason self-hosting Google Fonts with Fontsource makes preloading practical where the hosted Google Fonts CSS does not.

When is prefetch actually worth it for fonts? Only when a future navigation needs a weight the current page does not render — for example a marketing display face used solely on the next page in a known funnel. It runs at the lowest priority during idle time, so it never competes with the current page's LCP, and the cached file makes the next navigation paint instantly. Do not use it for fonts the current page also needs; that case belongs to preload.

Can I combine preload with fetchpriority? Yes, and for a page with several high-priority resources competing for the same slot — a hero image and a heading font, for instance — it is often necessary. <link rel="preload" as="font" fetchpriority="high"> tells the browser this particular preload should win contention over other High-priority requests. Without it, the browser's own heuristics decide the tie, which is not always what you want if the font gates a larger share of visible text than the image does.

What happens if I preload a font format the browser does not support? The browser skips the preload entirely and downloads nothing for that <link> — it checks the type attribute against its supported formats before fetching. This is why a preload should only be used for a single, known-good WOFF2 file rather than a multi-format fallback chain; format negotiation belongs in @font-face src with multiple url()/format() pairs, not in preload hints, since a preload can target exactly one URL.

Does dns-prefetch do anything preconnect does not already cover? It is a strict subset — preconnect performs DNS resolution and the TCP/TLS handshake, while dns-prefetch only resolves DNS. Pair them because preconnect has a lower browser connection cap (roughly four to six origins) and older or constrained browsers may silently drop excess preconnects while still honoring the cheaper dns-prefetch, which has a much higher practical ceiling.

Related