Command Palette

Search for a command to run...

Command Palette

Search for a command to run...

Blog
PreviousNext

Optimizing font loading for better performance

A practical approach to loading web fonts without slowing down initial page render.

At a glance, adding web fonts seems simple: pick a font, copy the provided snippet, drop it into your project, and confirm it works. This is especially common with Google Fonts, where developers routinely paste a <link> into the <head>.

But performance tools like Lighthouse often flag this exact setup.

Lighthouse score

The issue with default font loading

When font stylesheets are placed in the <head>, Lighthouse identifies them as render-blocking resources, sometimes delaying page rendering by about a second. This can be confusing since the setup follows standard documentation and best practices.

So what's the problem?

The goal is to:

  • Remove font stylesheets from blocking rendering
  • Improve performance scores
  • Avoid the "flash of unstyled text" (FOUT)

All of this can be achieved using plain HTML, CSS, and JavaScript, making it applicable across frameworks.

What "render-blocking" actually means

When a browser loads a page, it builds a render tree using:

  • The DOM (HTML structure)
  • The CSSOM (CSS rules)

This process is part of the critical render path, which determines how quickly content appears on screen.

Critical resources

Before rendering, the browser must:

  1. Load and parse HTML
  2. Load and parse all linked CSS

Even small font stylesheets can slow things down because:

  • The stylesheet must load first
  • Then the font files referenced inside must also be fetched
@font-face {
  font-family: "Merriweather";
  src:
    local("Merriweather"),
    url(https://fonts.gstatic.com/...) format("woff2");
}

Although lightweight, these files still:

  • Become part of the critical render path
  • Delay visible content

Why this matters for users

Users care about content first. If a page stays blank while waiting for fonts, especially on slow connections, they may leave.

A better approach is to:

  • Prioritize essential resources (HTML and critical CSS)
  • Load fonts later

This ensures content appears quickly, even if styling isn't fully applied yet.

A more efficient font loading strategy

A widely accepted method includes four steps:

  1. Preconnect to the font source
  2. Preload the font stylesheet
  3. Load fonts asynchronously after rendering
  4. Provide a fallback for users without JavaScript
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
 
<link
  rel="preload"
  as="style"
  href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap"
/>
 
<link
  rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap"
  media="print"
  onload="this.media='all'"
/>
 
<noscript>
  <link
    rel="stylesheet"
    href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap"
  />
</noscript>

Why this works

  • preconnect speeds up network setup
  • preload fetches the stylesheet early
  • media="print" lowers priority so it doesn't block rendering
  • onload switches it back to normal once ready

This removes fonts from the critical rendering path and improves Lighthouse results.

The tradeoff: FOUT

Loading fonts later introduces Flash of Unstyled Text (FOUT), where fallback fonts appear briefly before switching to the web font.

Flash of Unstyled Text (FOUT)

This can also cause layout shifts.

Reducing FOUT

To make the transition less noticeable:

  1. Pick a fallback font similar to your web font
  2. Match styling (size, spacing, line height)
  3. Smoothly switch styles once the font loads

Tools like Font style matcher can help align fallback and web fonts.

Detecting font load completion

The CSS Font Loading API allows detection of when a font is ready.

document.fonts.check("12px 'Merriweather'")

This returns true if the font is loaded.

Requirements

  • At least one element must use the font
  • The font name must match exactly

Example

html

<body class="no-js">
  <!-- Content -->
 
  <div
    aria-visibility="hidden"
    class="hidden"
    style="font-family: '[web-font-name]'"
  >
    <!-- non-breaking space -->
  </div>
 
  <script>
    document.body.classList.remove("no-js")
  </script>
</body>

css

body:not(.wf-merriweather--loaded):not(.no-js) {
  font-family: [fallback-font];
}
 
.wf-merriweather--loaded,
.no-js {
  font-family: "[web-font-name]";
}
 
.hidden {
  position: absolute;
  overflow: hidden;
  clip: rect(0 0 0 0);
  height: 1px;
  width: 1px;
}

javascript

var interval = null
 
function fontLoadListener() {
  var hasLoaded = false
 
  try {
    hasLoaded = document.fonts.check('12px "[web-font-name]"')
  } catch (error) {
    console.info("Font API error", error)
    fontLoadedSuccess()
    return
  }
 
  if (hasLoaded) {
    fontLoadedSuccess()
  }
}
 
function fontLoadedSuccess() {
  if (interval) {
    clearInterval(interval)
  }
  // Apply class for loaded font
}
 
interval = setInterval(fontLoadListener, 500)

Next.js approach (Simpler and built-in)

Unlike manual setups, Next.js already handles most font optimization for you using next/font.

Example using Google Fonts

import { Merriweather } from "next/font/google"
 
const merriweather = Merriweather({
  subsets: ["latin"],
  display: "swap",
})
 
export default function App({ Component, pageProps }) {
  return (
    <main className={merriweather.className}>
      <Component {...pageProps} />
    </main>
  )
}

Why this is better

Next.js automatically:

  • Self-hosts font files (no external CDN dependency)
  • Eliminates render-blocking requests
  • Applies font-display: swap to reduce invisible text
  • Preloads fonts efficiently
  • Avoids layout shifts by optimizing fallback behavior

In other words, it handles:

  • Preloading
  • Performance optimization
  • FOUT mitigation

…without needing custom scripts or complex setups.

Conclusion

Content should always load first. Fonts are non-essential and can be deferred without affecting usability.

Key ideas:

  • Avoid render-blocking font loading
  • Load fonts asynchronously
  • Use fallback fonts effectively
  • Smooth transitions to reduce FOUT

If you're using a framework like Next.js, much of this complexity is handled for you automatically. Otherwise, a manual setup with preload, async loading, and font detection gives you full control over performance and user experience.