EdgeCases Logo
Nov 2025
CSS
Deep
7 min read

Font Preloading: When rel=preload Backfires

Missing crossorigin causes double downloads; too many preloads delay LCP—use surgically

css
fonts
preload
performance
resource-hints
optimization
edge-case

Add <link rel="preload" as="font"> to speed up font loading—watch Chrome DevTools warn "resource was preloaded but not used" and the browser download your font twice. Forget the crossorigin attribute on same-origin fonts, and preload becomes useless. Preload too many fonts, and you'll delay your LCP image by seconds. Font preloading isn't automatic performance—it's a sharp tool that can cut both ways.

What Font Preloading Actually Does

Browsers discover font requirements late in the rendering process—after parsing HTML, CSS, and building the render tree. By that time, critical rendering has already started. rel="preload" moves font discovery to HTML parse time, initiating downloads immediately.

<!-- Normal font loading: discovered after CSS parsing -->
@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom.woff2') format('woff2');
}

/* Font download starts when browser needs to render text */
/* Timeline: HTML → CSS → Render Tree → Font Request (~500ms delay) */

<!-- Preloaded font: discovered during HTML parsing -->
<link rel="preload"
      href="/fonts/custom.woff2"
      as="font"
      type="font/woff2"
      crossorigin>

/* Font download starts immediately during HTML parse */
/* Timeline: HTML + Font Request (parallel) → CSS → Render Tree */

The gain: fonts start downloading 300-800ms earlier. The cost: preloaded resources have highest priority, competing with critical resources like CSS and LCP images.

The Mandatory crossorigin Attribute

Font files are always fetched with CORS (Cross-Origin Resource Sharing) mode—even for same-origin fonts. This is a browser security requirement, not a server configuration issue.

Why crossorigin Is Required

When browsers request fonts via @font-face, they use anonymous CORS mode (no credentials, no cookies). If you preload a font without crossorigin, the browser makes two requests:

  1. Preload request: No CORS, includes cookies (normal mode)
  2. Actual font request: Anonymous CORS, no cookies

Browsers consider these different resources due to credential mode mismatch. The preloaded response can't be reused, causing a duplicate download.

<!-- WRONG: Missing crossorigin -->
<link rel="preload"
      href="/fonts/custom.woff2"
      as="font">

/* Chrome DevTools warning:
   "The resource was preloaded using link preload but not used within a few
   seconds from the window's load event. Please make sure it has an appropriate
   'as' value and it is preloaded intentionally." */

/* Network tab shows TWO requests for the same font! */

<!-- CORRECT: Include crossorigin -->
<link rel="preload"
      href="/fonts/custom.woff2"
      as="font"
      crossorigin>

/* Single request, preloaded response reused */

crossorigin="" vs crossorigin="anonymous"

Both work identically for fonts. The attribute presence is what matters:

<link rel="preload" as="font" href="/font.woff2" crossorigin>
<link rel="preload" as="font" href="/font.woff2" crossorigin="">
<link rel="preload" as="font" href="/font.woff2" crossorigin="anonymous">

/* All three are equivalent */

Never use crossorigin="use-credentials"—fonts don't use credentialed requests. This will also cause double downloads.

When Preloading Hurts Performance

1. Competing with LCP Images

Preloaded fonts get "highest" priority in Chrome's resource scheduler—same as render-blocking CSS. If you preload 3 fonts (150KB total) and your LCP image is 80KB, fonts download first, delaying LCP.

<!-- This delays LCP -->
<link rel="preload" href="/fonts/regular.woff2" as="font" crossorigin>
<link rel="preload" href="/fonts/bold.woff2" as="font" crossorigin>
<link rel="preload" href="/fonts/italic.woff2" as="font" crossorigin>

<!-- Hero image discovered late, queued behind 3 fonts -->
<img src="/hero.jpg" class="hero">

/* Timeline on slow 3G:
   0-800ms: Fonts download (150KB)
   800-1200ms: Hero image downloads (80KB) ← LCP delayed!
*/

Fix: Preload only critical fonts (1-2 max). Let secondary fonts load normally.

2. Preloading Unused Fonts

Preload downloads fonts immediately, regardless of whether they're used on the page. If you preload a font referenced in CSS but not rendered (due to media queries, display:none elements, etc.), you waste bandwidth.

<!-- Preload italic font -->
<link rel="preload" href="/fonts/italic.woff2" as="font" crossorigin>

<!-- But page has no italic text -->
body {
  font-family: 'CustomFont', sans-serif;
  font-style: normal; /* Italic font never used */
}

/* Wasted bandwidth: downloaded but not rendered */

Rule: Only preload fonts you're 100% certain will render above-the-fold.

3. HTTP/2 Push vs Preload

HTTP/2 Server Push sends resources before the browser requests them. Combining Push + Preload can double-download resources:

  • Server pushes font.woff2
  • Browser parses HTML, sees <link rel="preload">
  • Browser initiates new request (doesn't know server is pushing)
  • Two copies download simultaneously

Fix: Don't use both. If your server uses HTTP/2 Push for fonts, remove rel="preload" from HTML.

Correct Preload Implementation

Minimal Preload Strategy

<!-- Preload ONLY primary body font (most critical) -->
<link rel="preload"
      href="/fonts/inter-regular.woff2"
      as="font"
      type="font/woff2"
      crossorigin>

<!-- Let other weights load normally -->
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-regular.woff2') format('woff2');
  font-weight: 400;
  font-display: optional; /* Pairs well with preload */
}

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-bold.woff2') format('woff2');
  font-weight: 700;
  /* NO preload - this loads on-demand */
}

Include type Attribute

The type attribute helps browsers skip fonts they don't support (older browsers ignoring WOFF2):

<link rel="preload"
      href="/fonts/font.woff2"
      as="font"
      type="font/woff2"
      crossorigin>

/* Browsers that don't support WOFF2 skip the preload */

Preload for Third-Party Fonts

Google Fonts, Adobe Fonts, and other third-party providers complicate preloading. You need to preload both the CSS and font files:

<!-- Step 1: Preload Google Fonts CSS -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload"
      href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"
      as="style">
<link rel="stylesheet"
      href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap">

<!-- Step 2: Preload actual font files (harder - URLs are dynamic) -->
<!-- Google Fonts change URLs based on browser, not practical to preload -->

/* Better: Self-host fonts to control preloading */

Recommendation: Self-host Google Fonts with tools like google-webfonts-helper for maximum preload control.

Debugging Preload Issues

Chrome DevTools Network Tab

  1. Open Network tab, filter to "Font"
  2. Look for duplicate requests (same URL requested twice)
  3. Check "Initiator" column: preloaded fonts show "Preload" initiator
  4. If initiator says "Parser" instead, preload didn't work

Console Warnings

Chrome logs specific warnings for preload problems:

  • "The resource was preloaded but not used": Missing crossorigin or wrong MIME type
  • "Preload triggered double download": Credential mode mismatch (crossorigin issue)
  • "Preload not used within a few seconds": Font not actually rendered on page

Lighthouse Audit

Lighthouse's "Preload key requests" audit identifies fonts that should be preloaded. But take it with skepticism—Lighthouse suggests preloading all fonts, which can hurt LCP. Use judgment.

Preload vs Prefetch vs Preconnect

preload: "Download this now, you'll need it in 100ms."

<link rel="preload" href="/font.woff2" as="font" crossorigin>

Use for critical fonts used above-the-fold.

prefetch: "Download this when idle, you might need it later."

<link rel="prefetch" href="/font.woff2" as="font" crossorigin>

Use for fonts on next-page navigation. Lower priority than preload.

preconnect: "Establish connection to this origin early."

<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

Use for third-party font origins (Google Fonts, etc.). Saves DNS + TCP + TLS handshake time.

Production Checklist

  1. Preload 1-2 fonts maximum: Primary body font + optional headline font. No more.
  2. Always include crossorigin: Even for same-origin fonts. No exceptions.
  3. Include type="font/woff2": Helps older browsers skip unsupported formats.
  4. Pair with font-display: optional: Preload gets fonts in the ~100ms window, avoiding swaps.
  5. Self-host fonts: Third-party fonts complicate preloading. Self-hosting gives full control.
  6. Test on slow connections: Lighthouse "Slow 4G" throttling reveals priority conflicts.
  7. Monitor LCP: If LCP regresses after adding preloads, you're preloading too much.

The Modern Font Loading Stack

Combining the right techniques gives optimal performance:

<!-- HTML: Preload critical font -->
<link rel="preload"
      href="/fonts/inter-regular.woff2"
      as="font"
      type="font/woff2"
      crossorigin>

<!-- CSS: Use font-display optional + size-adjust fallback -->
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-regular.woff2') format('woff2');
  font-display: optional; /* No swap = no CLS */
}

@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 97%;
  ascent-override: 90%;
  descent-override: 22%;
}

body {
  font-family: 'Inter', 'Inter Fallback', sans-serif;
}

/* Result:
   - Preload starts font download immediately
   - font-display: optional uses it if loaded in ~100ms
   - If not loaded in time, size-adjusted Arial shows (no CLS)
   - LCP not delayed (only 1 font preloaded)
   - First-time visitors get great experience
   - Repeat visitors get cached font with zero delay */

Font preloading is powerful but unforgiving. Use it surgically: one or two critical fonts, always with crossorigin, and always measured against LCP impact.

Advertisement

Related Insights

Explore related edge cases and patterns

CSS
Surface
Dynamic Fonts in Web Development
5 min
CSS
Deep
Font Loading: The FOUT, FOIT, and CLS Dilemma
7 min
CSS
Deep
size-adjust: Eliminating Font Swap Layout Shifts
7 min

Advertisement