How to Preload Web Fonts Correctly with crossorigin
This guide addresses one of the most common resource-hint bugs, within Resource Hints & Early Hints in Advanced Caching Strategies & CDN Architecture. Web fonts are discovered late — only after the CSS that references them is downloaded and parsed, and often only once the browser knows some text uses them. Preloading the font that renders your LCP heading lets the request start during HTML parsing instead, which can save several hundred milliseconds on mobile.
But fonts are always fetched in CORS mode, even from your own origin. A <link rel="preload" as="font"> without the crossorigin attribute fetches the file in no-CORS mode, the browser cannot reuse that response for the CSS's CORS request, and downloads the font a second time. The preload then wastes bandwidth on the critical path and the console warns about an unused preload. One attribute separates a useful hint from a harmful one.
Rapid Diagnosis
- Check the Network panel for the same font URL requested twice — once with initiator "preload", once from the stylesheet.
- Read the console. "The resource … was preloaded using link preload but not used within a few seconds" for a font usually means a mode or URL mismatch.
- Compare URLs exactly. The preloaded URL must match the
srcin@font-face, including query strings and the chosen format. - Check
type="font/woff2". Without it, browsers that do not support the format may still download it.
Root Cause Analysis
1. Missing crossorigin. The most common cause of double font downloads.
2. URL mismatch. The preload points at a different file than CSS uses (a different subset, version hash or format).
3. Preloading too many fonts. Several preloaded weights compete with CSS and the LCP image, delaying everything.
4. Preloading fonts not used above the fold. Bandwidth spent early for text the user cannot see yet.
Step-by-Step Resolution
1. Preload only the font the LCP text needs, with crossorigin
<link rel="preload" href="/fonts/brand-sans-700-latin.4c1a.woff2" as="font" type="font/woff2" crossorigin>
<!-- trade-off: crossorigin is required even for same-origin fonts because fonts
are always fetched in CORS mode. For cross-origin hosts, the font server must
also send Access-Control-Allow-Origin, or the font will be blocked. -->
Expected outcome: one font request, starting during HTML parsing.
2. Match the @font-face src exactly
@font-face {
font-family: "Brand Sans";
src: url("/fonts/brand-sans-700-latin.4c1a.woff2") format("woff2");
font-weight: 700;
font-display: swap;
}
/* trade-off: hashed filenames must be generated identically for the preload and
the CSS; let the build tool emit both from one source of truth. */
3. Limit preloads to one or two files
Preload the weight and subset used by above-the-fold text (usually the heading weight), and let others load via CSS. Each additional preload pulls bandwidth from CSS and the LCP image.
4. Set CORS headers on cross-origin font hosts
If fonts come from a separate host, it must send Access-Control-Allow-Origin (a specific origin or *). Without it, fonts fail to load whether or not they are preloaded.
Verification
The Network panel should show each preloaded font once, initiated by the preload, with no second request from CSS. The console should be free of unused-preload warnings for fonts. In a throttled trace, the font should finish before or around the time CSS finishes, and text LCP should improve — typically by 100–400ms on mobile when the font was previously discovered late.
Worked Example: Double Font Downloads
A SaaS landing page preloaded three font weights without crossorigin. Every page view downloaded six font files on the critical path — 140KB — and LCP on mobile was 3.1s. Adding crossorigin removed the duplicates; reducing preloads to the single heading weight removed another 48KB from the critical path. LCP p75 fell to 2.3s, and the console warnings disappeared.
Preloading vs Inlining vs Font-Display
Preloading is one of three font tools, and they interact. font-display decides what users see while the font loads (swap shows fallback text immediately; optional uses the font only if it arrives very quickly). Preloading makes the font arrive sooner. Inlining small fonts as data URIs in CSS removes the request entirely but bloats render-blocking CSS and prevents separate caching — rarely worth it except for tiny icon subsets. A common robust combination: preload the heading font, font-display: swap with metric-matched fallbacks for headings, and font-display: optional for body text — see font-display swap vs optional for LCP.
Common Mistakes
- Omitting
crossorigin. Double downloads. - Preloading WOFF and WOFF2. Modern browsers only need WOFF2; preloading both wastes bytes.
- Preloading from Google Fonts' CSS URL. The font file URLs are not known until the CSS loads; self-host to preload reliably.
- Preloading on every page. Only templates where the font is used above the fold benefit.
Edge Cases
crossorigin="use-credentials". Only needed if the font server requires credentials, which is rare and needs matching CORS headers.
Variable fonts. One file covers all weights, so a single preload serves every heading; weigh its larger size against the LCP benefit.
Early Hints. Font preloads work in 103 Early Hints Link headers too, with crossorigin expressed as an attribute in the header (; crossorigin).
unicode-range subsets. Preload ignores unicode-range; preload only the subset the page's language needs.
FAQ
Why do same-origin fonts need crossorigin?
The CSS Fonts specification requires fonts to be fetched in CORS mode regardless of origin. The preload must use the same mode for the browser to match the two requests.
Does preloading a font hurt if it is not used?
Yes — it consumes bandwidth during the most critical part of the load, and Chrome warns about it. Preload only fonts used above the fold on that template.
Should I preload icon fonts?
Better to replace icon fonts with SVG; see migrating from icon fonts to SVG. If you keep one, preload it only if icons render in the first viewport.
Does fetchpriority apply to font preloads?
Font preloads are already high priority. Adding fetchpriority="high" changes little; lowering it can make sense for fonts used only below the fold that you still want early.
How do I preload fonts in Next.js or Nuxt?
Framework font modules (next/font, @nuxt/fonts) self-host and preload fonts automatically with the correct attributes; prefer them over hand-written links.
Is preconnect enough instead of preload for third-party fonts?
Preconnect saves connection time but the font request still waits for CSS. Preload starts the request immediately. For the LCP font, preload (with self-hosting) wins.
How do I preload fonts referenced by a third-party stylesheet?
You cannot reliably preload what you do not know the URL of. Self-host the fonts so URLs are stable and known at build time, then preload them; this is the main reason self-hosting usually beats preconnecting to a font CDN.
What does a correct font preload look like in the Network panel?
One request per font file, with the Initiator column showing the preload link (or the Early Hints response), priority High, status 200, and no second request for the same URL when the stylesheet is parsed. If you see the font twice, compare the request modes in the Headers tab — Sec-Fetch-Mode: cors must appear on the preload too.
Related
- Fixing unused preload warnings in the console — the broader class of preload mismatches.
- Subsetting web fonts with unicode-range — making the preloaded file small.
- Fixing LCP when the H1 is the LCP element — where font timing decides LCP.