How to Preload Fonts and Fix First Contentful Paint Before It Kills Your Rankings

How to Preload Fonts and Fix First Contentful Paint Before It Kills Your Rankings

Your page feels fast. The HTML loads, the layout snaps into place — but the text just… doesn’t show up. For a full second, sometimes two, your beautiful heading font is invisible. Or worse, it flashes as system text first, then switches. Users see that. Google sees that. And your FCP score takes the hit.

I’m Rohan Ratnayake, and I’ve spent the last five years as a front-end performance engineer working on e-commerce and SaaS platforms where every 100ms of render delay has a real cost attached to it. I’ve watched dev teams spend weeks optimizing JavaScript bundles, compressing images, and refining caching headers — then wonder why their Lighthouse FCP score still sits in the orange. Nine times out of ten, the culprit is sitting right at the top of their <head>: a web font that the browser doesn’t even know about until it’s already parsing halfway down the CSS file.

I made that mistake on a client project early on. We’d done everything “right” — tight CSS, lean HTML, CDN delivery. But the custom heading font was buried in a stylesheet that loaded after three other render-blocking resources. The FCP was 2.8 seconds on a fast connection. We added one line of HTML and dropped it to 1.4 seconds in the next test. That’s the fix I’m going to walk you through.


Why Your Font Is Always Late to the Party

Browsers don’t discover fonts the way you’d expect. When a page loads, the browser parses HTML, then fetches and parses CSS, then builds the render tree — and only then does it figure out which fonts are actually needed for the visible text. By that point, the font request hasn’t even started yet.

This is called late font discovery, and it’s the default behavior unless you explicitly tell the browser otherwise. The browser has to:

  1. Parse your HTML
  2. Request and download your CSS file
  3. Parse the CSS to find the @font-face rule
  4. Request the font file
  5. Download it
  6. Render the text

Each step waits for the previous one. On a typical site, that chain pushes your heading font request 600–900ms into the page load — even on a decent connection. That gap is exactly what First Contentful Paint is measuring against you.

ALSO READ:  FOUT vs. FOIT Explained: What Your Browser Is Actually Doing Before a Font Loads

The One Tag That Fixes This

The <link rel="preload" as="font"> tag is a browser hint that says: go get this file right now, before you’ve even parsed my CSS. It moves the font request to the very beginning of the network queue, parallel with the HTML itself.

Here’s what it looks like in practice:

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

That goes inside your <head>, before your stylesheets. Not after. Not at the bottom of the page. Before.

Each attribute is doing real work here:

AttributeWhat It DoesWhat Happens If You Skip It
rel="preload"Tells browser to fetch this resource earlyBrowser discovers font late via CSS
as="font"Sets correct request priority and headersBrowser fetches it as a generic resource, lower priority
type="font/woff2"Lets unsupported browsers skip the fetchOlder browsers waste a request on an unusable file
crossorigin="anonymous"Required for CORS-compliant font fetchingFont gets fetched twice — once as preload, once as actual request

That last one trips up a lot of developers. If you skip crossorigin, the browser actually makes two separate network requests for the same font file. You’ll see it in DevTools as a duplicate entry. Your preload effectively does nothing.


WOFF2 Is the Only Format Worth Preloading

Don’t preload WOFF, TTF, or OTF. WOFF2 has 95%+ browser support as of 2024, and it compresses significantly better than older formats. A typical Inter Bold in TTF runs around 300KB. The WOFF2 version is usually under 80KB for the same character set.

If you’re still serving multiple formats for compatibility, your @font-face block probably looks like this:

@font-face{ 
  font-family: 'Inter';
  src: url('/fonts/inter-bold.woff2') format('woff2'),
       url('/fonts/inter-bold.woff') format('woff');
  font-weight: 700;
  font-display: swap;
 }

You preload only the WOFF2. The browser will fall back to WOFF on its own if it can’t handle WOFF2 — which at this point means IE11. You’re not building for IE11.


How Many Fonts Should You Actually Preload?

This is where most performance guides get vague. Here’s my actual rule: preload only the font used in above-the-fold text that appears on first paint.

If your H1 heading uses a custom font, preload it. If your body copy uses that same font at a different weight, preload that weight too — but only if it’s visible without scrolling. Everything below the fold can wait.

ALSO READ:  WOFF vs. WOFF2: The Font Format Decision That's Silently Slowing Your Site

Preloading too many fonts creates its own problem. Each preloaded resource competes for early bandwidth. If you preload four fonts, you’re potentially slowing down the HTML, CSS, and any critical render-blocking scripts that also need that early bandwidth.

Here’s a practical breakdown:

Font UseShould You Preload?Reason
H1/H2 on hero sectionYesDirectly affects FCP
Body text (above fold)Yes, if custom fontVisible on first paint
Nav / footer fontsNoUsually not render-critical
Fonts used only in modalsNoNot on initial render
Icon fonts (e.g., Font Awesome)NoUse SVG instead

I typically preload one or two font files maximum per page. More than that, and you’re often making your FCP worse, not better, especially on mobile connections.


Self-Hosting vs. Google Fonts — This Changes Your Setup

If you’re using Google Fonts via their standard <link> embed, you can’t directly use <link rel="preload"> on the font files because the actual WOFF2 URLs are served from Google’s CDN with versioned paths that change. You don’t control them.

You have two real options:

Option 1: Self-host the font files. Download the WOFF2 file directly (Google Fonts lets you do this, or use a tool like google-webfonts-helper), host it on your server or CDN, and then preload it with a path you control. This is the setup I use on almost every project now.

Option 2: Use the font-display: swap CSS property as a partial fix. This doesn’t speed up font discovery, but it tells the browser to render text in a fallback system font immediately and swap it once the custom font loads. It won’t improve your FCP score, but it eliminates the invisible text problem (FOIT — Flash of Invisible Text). According to the MDN Web Docs on font-display, swap gives the font an infinite swap period, meaning text is always visible even if the font loads slowly.

If FCP is your goal, self-hosting plus preload is the only real fix. font-display: swap is a fallback for when preloading isn’t feasible.


Validating That Your Preload Is Actually Working

After you add the tag, don’t just assume it worked. Check it in two ways.

In Chrome DevTools: Open the Network tab, reload the page, and filter by “Font.” Your preloaded font should appear near the top of the waterfall — close to the HTML document request — not buried midway through. If it’s still low in the list, your preload tag is either placed incorrectly or missing the crossorigin attribute.

ALSO READ:  Stop Serving 5 Font Files: How CSS Variable Fonts Cut Your Load Time in Half

In PageSpeed Insights / Lighthouse: Look for the “Eliminate render-blocking resources” and “Preload key requests” audit sections. If your font was previously flagged under “Preload key requests” and now it’s gone, the tag is working. You should also see the FCP time drop.

A rough benchmark from projects I’ve worked on:

ScenarioTypical FCP Impact
No preload, no font-display2.4–3.2s FCP on 4G
font-display: swap only1.8–2.5s FCP on 4G
Preload + font-display: swap1.1–1.6s FCP on 4G
Preload + subsetting (Latin only)0.9–1.3s FCP on 4G

These are from real Lighthouse runs on real projects, not synthetic benchmarks. Your numbers will vary by hosting, file size, and total page weight — but the relative improvement pattern holds.


Subsetting: The Part Most Articles Skip

If you’re preloading a font, you should also be subsetting it. A full Latin + extended character set font might be 120KB. If your site is in English, you’re using maybe 30% of those characters. Subsetting strips out the glyphs you’ll never use.

Tools like Fonttools let you create a subset WOFF2 with only the characters you need. In practice, the unicode-range CSS property in your @font-face declaration handles the browser side of this — the browser only downloads the font file if the page actually contains characters in that range.

Combine a subsetted WOFF2 with a preload tag and you can often get heading font files under 20KB. That changes everything for mobile users on slower connections.


Three Questions I Get Asked Constantly

Does <link rel="preload"> work in all browsers? It has broad support across Chrome, Firefox, Safari, and Edge. The as="font" value specifically has good support, though older Safari versions (pre-12) had inconsistent behavior. For any browser that doesn’t support preload, the tag is simply ignored — it won’t break anything, it just won’t help either.

Do I need a separate preload tag for each font weight? Yes. Each font file (regular, bold, italic) is a separate WOFF2 file and requires its own preload tag. Only add preload tags for the weights that appear in your initial viewport. A 700-weight heading and a 400-weight subheading that both appear above the fold? Two preload tags. A 300-weight font used only in the footer? Don’t preload it.

Can this hurt my performance if I do it wrong? Yes. Unused preload tags generate a browser warning (“resource was preloaded but not used within a few seconds of the window’s load event”) and waste early-load bandwidth. If you preload a font that never gets used on that page — for example, because you have one preload tag in a global header but the font only appears on certain templates — you’re paying a performance cost for nothing.


The Actual Bottom Line

Every other FCP optimization you’re doing — your CSS minification, your server response time, your caching headers — gets undermined if your main heading font is discovered late in the render chain. The <link rel="preload" as="font"> tag is a single line of HTML that solves a problem most teams don’t even know they have.

Self-host your WOFF2 file, add the preload tag in <head> with the correct crossorigin attribute, pair it with font-display: swap in your @font-face declaration, and then actually verify it in DevTools. Don’t guess — look at the waterfall. If the font request is near the top, you fixed it.

Your next step: open Chrome DevTools on your site right now, go to the Network tab, filter by Font, and note exactly where in the waterfall your heading font request appears. That number tells you everything about whether you have a problem — and how bad it is.