Skip to content
nuxtMar 11, 2026· 4 min read

Self-Hosting Fonts in Nuxt for Better Performance

Jord

Jord

Product Engineer & Founder

If you've ever run a Lighthouse audit and seen "Eliminate render-blocking resources" with Google Fonts as the culprit, this is the fix. It took me about fifteen minutes to do on this site and it shaved a noticeable chunk off First Contentful Paint.

The issue with Google Fonts isn't that they're slow. Google's CDN is fast. The problem is the connection overhead. When your page loads, the browser has to:

  1. Parse the <link> tag or CSS @import
  2. DNS lookup for fonts.googleapis.com
  3. TCP + TLS handshake
  4. Fetch the CSS file
  5. DNS lookup for fonts.gstatic.com (different domain)
  6. TCP + TLS handshake again
  7. Fetch the actual font files

That's two DNS lookups and two connection setups before a single glyph renders. On a fast connection, it's maybe 200-400ms. On mobile or a spotty connection, it's noticeably more. Self-hosting eliminates all of that. The fonts come from your own domain, which the browser already has a connection to.

Step 1: Find Your Font Files

Go to the Google Fonts CSS URL you're currently using. The trick is that Google serves different formats based on your browser's User-Agent header. We want woff2, which is the smallest and most widely supported format.

You can fetch the CSS with a browser User-Agent to get the woff2 URLs:

curl -sL \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
  "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"

This returns a bunch of @font-face declarations with URLs pointing to fonts.gstatic.com. Each one covers a different unicode range — latin, latin-ext, cyrillic, greek, and so on.

For an English-language site, you only need latin and latin-ext. Ignore the rest.

Step 2: Download the Font Files

Create a directory in your Nuxt project's public/ folder:

mkdir -p public/fonts

Download the latin and latin-ext woff2 files from the URLs in the CSS output:

curl -sL -o public/fonts/inter-latin.woff2 "https://fonts.gstatic.com/s/inter/v20/..."
curl -sL -o public/fonts/inter-latin-ext.woff2 "https://fonts.gstatic.com/s/inter/v20/..."

The exact URLs will differ — copy them from the curl output in Step 1.

Step 3: Replace the @import

In your main CSS file, replace the Google Fonts import with local @font-face declarations:

/* Before */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');

/* After */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('/fonts/inter-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
    U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113,
    U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('/fonts/inter-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC,
    U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

A few things to note:

font-weight: 300 700 — If your font supports variable weights (Inter does), you can specify a range instead of declaring a separate @font-face for each weight. One declaration, one file, all weights.

font-display: swap — This tells the browser to show a fallback font immediately and swap in the custom font when it's loaded. This prevents the flash of invisible text (FOIT) that kills your FCP score.

unicode-range — This tells the browser to only download the file if characters from that range are actually used on the page. Latin-ext won't download unless your content includes those characters. Copy these ranges directly from the Google Fonts CSS output.

Step 4: Preload the Primary Font

For the font file that covers the characters most of your content uses (usually the latin range), add a preload hint in your nuxt.config.ts:

export default defineNuxtConfig({
  app: {
    head: {
      link: [
        {
          rel: 'preload',
          href: '/fonts/inter-latin.woff2',
          as: 'font',
          type: 'font/woff2',
          crossorigin: 'anonymous',
        },
      ],
    },
  },
})

This tells the browser to start fetching the font immediately, before it even parses the CSS. For your primary body font, this makes a real difference.

Don't preload every font file — just the one that's most critical for initial render. Preloading too many fonts defeats the purpose.

What About @nuxt/fonts?

Nuxt has an official @nuxt/fonts module that automates a lot of this. It intercepts Google Fonts requests and serves them locally. It's a good option if you want a hands-off approach.

I prefer doing it manually because it gives me full control over which subsets I include, which weights I ship, and how the @font-face declarations are structured. But either approach gets the same result.

The Performance Difference

On this site, self-hosting fonts reduced First Contentful Paint by around 200ms on a simulated 3G connection. On fast connections the difference is smaller, but it's still measurable.

More importantly, it eliminates a third-party dependency from your critical render path. Google Fonts has never gone down as far as I know, but the connection overhead is real and it's unnecessary when the alternative is a few static files in your public/ folder.

Final Thoughts

Self-hosting fonts is one of those performance wins that takes fifteen minutes and lasts forever. No ongoing maintenance, no build tooling changes, no dependencies to update. Download the files, write the @font-face declarations, and your site is permanently faster.

If your Lighthouse score matters to you — and if you care about SEO, it should — this is low-hanging fruit.

The Inner Circle

Stay in the loop.

Weekly insights on building resilient systems, scaling solo SaaS, and the engineering behind it all.

No spam. Just high-signal engineering.