Why Font Preload Needs the crossorigin Attribute
This guide is part of the Font Preloading & Resource Hints guide, itself under the Font Loading & Delivery Strategies area.
The problem: a preload that fetches the font twice
You add <link rel="preload" as="font" href="/fonts/inter-var.woff2" type="font/woff2"> to the <head> expecting the browser to warm the cache before the @font-face rule ever asks for the file. Instead, DevTools shows the font downloaded twice — once for the preload, once for the actual @font-face fetch — and the network waterfall gets worse, not better. The byte count for that single WOFF2 file doubles, and on a mobile connection the second copy can arrive after first paint, which is exactly the outcome preload was supposed to prevent.
The cause is a single missing attribute: crossorigin. Fonts are the one resource type where the browser always performs an anonymous CORS fetch, even when the file is same-origin. If the preload <link> doesn't declare crossorigin, its request uses a different fetch mode than the one the actual font load will use, the two requests land in different cache-key buckets, and the browser has no choice but to fetch the resource again. This single-attribute gotcha is one of the most common reasons a preload measured in a Lighthouse report shows "preloaded but not used" or "duplicate request."
This matters more than it looks like a one-line HTML fix would suggest. Font files are frequently among the largest render-blocking-adjacent assets on a page — a variable WOFF2 covering a full Latin weight range can easily run 80–150KB — so paying for that transfer twice is not a rounding error. On a throttled "Slow 4G" profile in DevTools, a 120KB variable font that should load once in roughly 400ms instead ties up the connection for closer to 800ms across two serialized fetches, because the second request can't start warming until the first has been discarded as unusable by the font engine. That is 400ms of extra time before the browser can paint the real text, directly inflating whatever Largest Contentful Paint number your hero heading contributes.
Prerequisites
- A
@font-facedeclaration already in your CSS, referencing a WOFF2 file (see Font Format Strategy if you haven't settled on WOFF2 as your primary format). - A
<link rel="preload">hint already present, or about to be added, for the same file. - Access to Chrome DevTools' Network panel to verify request counts before and after the fix.
- Familiarity with how the browser prioritizes
font-displayswap/optional periods, since a duplicated fetch effectively lengthens whichever period you've configured.
Why fonts are always fetched anonymously
The @font-face src fetch algorithm in the CSS Fonts spec mandates that font resources are always requested in crossorigin (CORS-anonymous) mode, with no credentials attached, regardless of whether the URL is same-origin or cross-origin. This is a deliberate security decision: fonts can leak information through glyph metrics and hinting data, so browsers treat every font fetch — including same-origin ones — as an anonymous, credential-less CORS request.
A <link rel="preload"> hint, on the other hand, defaults to a "no-cors" (or same-origin, depending on browser) fetch mode unless you tell it otherwise. When the preloaded resource is destined to be consumed as="font", the browser needs the preload's fetch to match — byte for byte, credentials mode included — the fetch that the font engine will perform later. If the two fetch modes differ, the Fetch spec's cache-matching algorithm treats them as different requests. The result is two separate network entries for the same URL: one triggered by the preload, one triggered by the actual glyph rasterization when text needs to render.
This is different from every other preloadable resource type. A preloaded script or stylesheet defaults to a fetch mode that already matches how the browser will eventually request it, so those hints work correctly with nothing more than rel="preload" and the right as value. Fonts break that pattern because the CSS Fonts spec deliberately overrides the destination's default fetch mode, and that override is invisible in the markup — there's nothing in a <link> tag that tells you fonts are special this way short of knowing the spec detail. That's precisely why this attribute is so easy to omit: the HTML looks complete and valid, the page renders, and only a careful look at the Network panel or a console warning reveals the duplicated request.
Implementation
The fix is to always pair as="font" with a bare crossorigin attribute, even for fonts hosted on your own origin:
<head>
<link
rel="preload"
href="/fonts/inter-var.woff2"
as="font"
type="font/woff2"
crossorigin
>
<link rel="stylesheet" href="/styles/main.css">
</head>
@font-face {
font-family: "Inter Var";
src: url("/fonts/inter-var.woff2") format("woff2-variations");
font-weight: 100 900;
font-display: swap;
}
body {
font-family: "Inter Var", system-ui, sans-serif;
}
Line by line:
rel="preload"tells the browser this is a high-priority fetch to start immediately, ahead of the parser discovering the@font-facerule in the stylesheet.as="font"sets the correctAcceptheader and — critically — tells the browser which "destination" this request is for, so it can match the fetch mode the eventual font consumer will use.type="font/woff2"lets browsers that don't support WOFF2 skip the preload entirely, avoiding a wasted fetch for a format they can't use.crossorigin(written bare, which is equivalent tocrossorigin="anonymous") forces the preload's fetch mode to CORS-anonymous with no credentials — matching what the@font-faceengine will request later. Without it, Chrome and Firefox both log a console warning along the lines of "was preloaded using link preload but not used within a few seconds," which is the tell-tale sign of this exact bug.- The stylesheet link comes after the preload so the preload's
fetchpriorityisn't undercut by the CSSOM-blocking stylesheet request; see fetchpriority for Font Requests for tuning request priority further.
Same-origin fonts need crossorigin just as much as fonts served from a CDN. Developers frequently assume the attribute is only for cross-origin resources — it is named after CORS, after all — but the font destination is the exception: the browser's fetch-mode matching for as="font" ignores same-origin/cross-origin distinctions and always requires the anonymous CORS mode.
Defensive variant: preloading from a CDN or subdomain
When fonts are served from a separate origin — a dedicated font CDN, a static. subdomain, or a third-party host — the crossorigin requirement compounds with actual CORS policy. The CDN must also return an Access-Control-Allow-Origin header that matches your page's origin (see Serving Fonts from a CDN with Correct CORS for the header configuration), or the preloaded bytes will be discarded as an opaque, unusable response:
<link rel="preconnect" href="https://static.example.com" crossorigin>
<link
rel="preload"
href="https://static.example.com/fonts/inter-var.woff2"
as="font"
type="font/woff2"
crossorigin
fetchpriority="high"
>
# static.example.com font response headers
add_header Access-Control-Allow-Origin "https://www.example.com" always;
add_header Cache-Control "public, max-age=31536000, immutable" always;
The preconnect hint also carries crossorigin because the TCP/TLS handshake it opens must be usable for the anonymous-mode font request that follows — a preconnect without crossorigin opens a separate connection that the CORS-mode font fetch can't reuse, defeating the purpose of preconnecting at all. If your CDN can't be configured with a permissive Access-Control-Allow-Origin, self-hosting the font file removes the CORS layer entirely, at the cost of losing any shared-cache benefit across sites using the same public CDN.
Verification
Open Chrome DevTools → Network, filter by Font, and reload the page with cache disabled. Before the fix, you'll see two rows for the same font URL: one initiated by "Preload" with a status like 200 and a small transfer size (often served from a fast local cache on repeat inspection), followed a moment later by a second 200 row initiated by "Other" or "CSS" with the full transfer size again. After adding crossorigin, only one row remains, and its Initiator column reads Preload while its Time reflects the fetch starting at document-head parse time rather than at CSSOM-construction time.
You can also confirm programmatically with the Resource Timing API:
const fontEntries = performance
.getEntriesByType("resource")
.filter((e) => e.name.endsWith(".woff2"));
if (fontEntries.length > 1) {
console.warn(
`Expected 1 font request, got ${fontEntries.length} — check crossorigin on the preload link.`
);
}
Run this after window.load fires; a length greater than one for a single font file is a reliable signal that a preload/fetch mismatch is still duplicating the download. Lighthouse's "Preload key requests" and "Avoid chaining critical requests" audits will also flag the wasted second fetch, and the DevTools console prints the standard "preloaded... but not used within a few seconds" warning whenever the mismatch persists.
For a more precise check, inspect the initiatorType and transferSize fields on each matching entry rather than just the count:
performance
.getEntriesByType("resource")
.filter((e) => e.name.endsWith(".woff2"))
.forEach((e) =>
console.log(e.name, e.initiatorType, e.transferSize, e.duration.toFixed(1))
);
A correctly configured preload shows exactly one entry with initiatorType: "link" and a transferSize matching the file's real compressed weight. If you instead see two entries — one "link" and a second "css" — with roughly equal transferSize values, the mismatch is still present and the fix hasn't taken effect, possibly because a service worker, a CDN redirect, or a stale HTML cache is serving the old markup without crossorigin.
Common Pitfalls
- Omitting
crossoriginon same-origin fonts. The most common version of this bug — developers preload a same-origin font, see no immediate error, and don't notice the duplicate fetch until they inspect the waterfall. Always addcrossoriginto every font preload, same-origin or not. - Adding
crossorigin="use-credentials"instead of anonymous. Fonts must be fetched without credentials; usinguse-credentials(or expecting cookies to ride along) causes the CORS check to fail unless the server explicitly allows credentialed requests withAccess-Control-Allow-Credentials, which most font CDNs do not set. Use the barecrossorigin(anonymous) form. - Preloading without a matching
preconnect crossoriginfor cross-origin fonts. The connection warm-up needs the same crossorigin mode as the font fetch it's meant to accelerate, or the browser opens a second connection and the preconnect's benefit is lost. - Mismatched
asvalue. Settingas="fetch"or omittingasentirely means the browser can't determine the correct fetch-mode/destination pairing, so it either ignores the preload's priority boost or, again, ends up fetching the resource twice under different destinations. - Missing
typeattribute causing wasted preloads in older engines. Withouttype="font/woff2", some browser versions preload a format they'll never use for a client that doesn't support WOFF2, wasting bandwidth on a request that gets discarded once the@font-facesrclist is evaluated for a supported format.
Frequently Asked Questions
Does crossorigin matter if the font is served from the exact same origin as the HTML document?
Yes. The CSS Fonts spec's fetch algorithm forces every font request into anonymous CORS mode regardless of origin, so the preload hint must match that mode with crossorigin even when there is no actual cross-origin boundary being crossed. This is the single most surprising part of the preload-font interaction and the reason the double-fetch bug shows up so often on self-hosted fonts.
What happens if I forget crossorigin — does the page break, or just get slower?
The page still renders correctly; nothing breaks visually. The cost is purely performance: the font downloads twice, doubling its contribution to total transfer bytes and delaying whichever fetch actually gets used to rasterize text, which can push back First Contentful Paint and Largest Contentful Paint on font-dependent hero text.
Do I need crossorigin on rel="prefetch" hints for fonts I'll need on the next page navigation?
Yes, for the same reason — prefetch and preload share the same destination-based fetch-mode matching. If you prefetch a font for a likely next page (see Preload vs Prefetch vs Preconnect for Fonts), keep as="font" and crossorigin on that hint too, or the prefetched bytes won't be reused when the next page's @font-face rule requests the file.
Does this attribute affect font-display behavior at all?
No — crossorigin only controls which cached response a fetch matches; it has no effect on the block/swap/fallback timing controlled by font-display values. A missing crossorigin just means the font arrives later (after two fetches instead of one), which indirectly makes whatever font-display period you've configured feel longer in practice.