Skip to content
intermediate css 13 min read

Web Fonts Guide: Google Fonts, Self-Hosting, and Performance

Learn how to use web fonts effectively while keeping your site fast and accessible.

web fonts google fonts font-face css web font performance

What Are Web Fonts and Why Do They Matter?

Web fonts let you load custom typefaces in the browser instead of relying on whatever fonts a user has installed. Done right, they make your site look polished and on-brand. Done wrong, they tank your performance and cause jarring layout shifts.

This guide walks you through every approach—from dropping in a Google Fonts link to self-hosting variable fonts—with real code you can use immediately.

Using Google Fonts

Google Fonts is the fastest way to get started with web fonts. It's free, handles licensing, and serves files from Google's CDN with solid caching headers.

The most important rule: only load what you actually use. Every extra weight or style adds to your page's total download size.

<!-- Add these to your <head>, in this order -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link 
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&family=Lora:ital,wght@0,400;0,700;1,400&display=swap" 
  rel="stylesheet"
>

The rel="preconnect" hints tell the browser to open a connection to Google's servers early, before it processes the font stylesheet. This shaves meaningful milliseconds off your load time.

The display=swap parameter at the end of the font URL maps to the CSS font-display: swap property. It tells the browser to render text immediately using a fallback font, then swap in your custom font once it's loaded—preventing invisible text during load.

Once you've added the <link> tags, apply your fonts in CSS with a sensible fallback stack:

body {
  font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
  font-size: 16px;      /* Never go below 16px for body text */
  line-height: 1.5;     /* WCAG recommends at least 1.5 */
  font-weight: 400;
}

h1, h2, h3 {
  font-family: 'Lora', Georgia, 'Times New Roman', serif;
  font-weight: 700;
  line-height: 1.2;
}

/* Use weight variation instead of separate fonts for emphasis */
strong, b { font-weight: 700; }
.medium   { font-weight: 500; }

The fallback stack matters. If the custom font fails to load or takes too long, system-ui on macOS renders San Francisco, while -apple-system is the iOS equivalent. Windows users get Segoe UI. Your layout stays intact.

Self-Hosting with @font-face

Self-hosting gives you full control over caching, eliminates a third-party dependency, and can improve performance for users on slow connections to Google's servers. The tradeoff is that you manage the files yourself.

Basic @font-face Setup

Download your font files (.woff2 is the format you want in 2024—it has the best compression and universal modern browser support) and reference them with @font-face:

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

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

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

Always include font-display: swap in every @font-face block. Without it, browsers default to a "block" period where text is completely invisible until the font loads—a terrible experience on slow connections.

Variable Fonts: The Modern Approach

Variable fonts are the biggest web font performance win available today. A single variable font file replaces five or more static font files, typically at a smaller total size. You get the entire weight range (100–900) and sometimes optical size and width axes, all from one HTTP request.

@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter-VariableFont.woff2') format('woff2');
  font-weight: 100 900;   /* Declares the supported weight range */
  font-style: normal;
  font-display: swap;
}

/* Now you can use any weight, not just the ones you pre-loaded */
body        { font-weight: 400; }
.medium     { font-weight: 500; }
h2          { font-weight: 600; }
h1          { font-weight: 700; }
.display    { font-weight: 800; }

You can grab Inter's variable font directly from Google Fonts Variable or from the official Inter GitHub releases. Store it in /public/fonts/ if you're on Next.js or Vite, or /static/fonts/ on most other setups.

For older browser fallback support, use @supports:

/* Modern browsers: variable font */
@supports (font-variation-settings: "wght" 400) {
  body {
    font-family: 'Inter', system-ui, sans-serif;
    font-weight: 400;
  }
  
  h1 { font-weight: 700; }
  h2 { font-weight: 600; }
}

/* Older browsers: static fallback */
@supports not (font-variation-settings: "wght" 400) {
  body {
    font-family: 'Inter-Regular', system-ui, sans-serif;
  }
  
  h1, h2 {
    font-family: 'Inter-Bold', system-ui, sans-serif;
  }
}

Variable fonts are supported in Chrome 62+, Firefox 62+, Safari 11+, and Edge 17+, which covers essentially all active browsers as of now.

Font Performance Optimization

Preloading Critical Fonts

If a font is used above the fold—your body text font especially—preload it. This tells the browser to fetch it at high priority before it parses your CSS:

<head>
  <!-- Preload only the most critical font file -->
  <link 
    rel="preload" 
    href="/fonts/Inter-VariableFont.woff2" 
    as="font" 
    type="font/woff2" 
    crossorigin
  >
  <!-- Stylesheet loads after -->
  <link rel="stylesheet" href="/styles/main.css">
</head>

The crossorigin attribute is required even for same-origin fonts when using rel="preload". Without it, the browser fetches the font twice.

Only preload one or two fonts. Preloading everything defeats the purpose—you'd be competing for bandwidth with resources the browser already prioritized.

Subsetting to Reduce File Size

Font subsetting means stripping out characters you don't need. If your site is English-only, you don't need Cyrillic, Greek, or extended Latin glyphs. A subsetted font can be 60–80% smaller than the full character set.

Google Fonts handles subsetting automatically. When you add &subset=latin to the URL, Google serves only Latin characters:

<link 
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&subset=latin&display=swap" 
  rel="stylesheet"
>

For self-hosted fonts, use glyphhanger or pyftsubset to generate subsetted files during your build process.

font-display Values Explained

font-display controls exactly how the browser handles font loading. The right choice depends on your priorities:

Value Behavior Best for
swap Show fallback immediately, swap when ready Body text, headings
optional Use custom font only if already cached Non-critical decorative fonts
fallback 100ms block, then swap if loaded within 3s Performance-sensitive cases
block Hide text until font loads (up to 3s) Almost never—avoid this

For most situations, swap is the right call. optional is worth considering for fonts that only appear on some pages—if a returning visitor already has the font cached, it renders immediately; if not, the fallback is used and the custom font is silently dropped.

Responsive Typography

Web fonts don't exist in isolation—they need to work at every screen size. Use rem units for font sizes (relative to the root font-size, typically 16px) and media queries to scale up for larger viewports:

/* Base sizing — mobile first */
:root {
  font-size: 16px;
}

body {
  font-size: 1rem;      /* 16px */
  line-height: 1.5;
  max-width: 70ch;      /* Limits line length to ~70 characters */
  margin-inline: auto;
}

h1 { font-size: 1.75rem; }  /* 28px on mobile */
h2 { font-size: 1.375rem; } /* 22px on mobile */
h3 { font-size: 1.125rem; } /* 18px on mobile */

/* Scale up for wider screens */
@media (min-width: 768px) {
  body  { font-size: 1.125rem; }  /* 18px */
  h1    { font-size: 2.25rem; }   /* 36px */
  h2    { font-size: 1.75rem; }   /* 28px */
  h3    { font-size: 1.375rem; }  /* 22px */
}

The max-width: 70ch trick on body text is underrated. ch is relative to the width of the 0 character in the current font, so 70ch naturally limits your line length to the recommended 45–75 characters regardless of font size or screen width.

Measuring Font Performance

After implementing web fonts, verify the impact. Open Chrome DevTools → Network tab → filter by "Font" to see which files load, their sizes, and timing. You want:

  • No font files over 50KB for your primary typefaces
  • First Contentful Paint (FCP) not delayed by font loading
  • No Cumulative Layout Shift (CLS) from font swapping

Use the Performance panel's "Timings" section to check if fonts are blocking rendering. If your FCP is delayed by font loads, add rel="preload" for the critical file.

WebPageTest's "Font Loading" report gives you a waterfall view that clearly shows if fonts are on the critical path. This is worth running before and after any font changes.

If you're working on your CSS while optimizing fonts, the CSS Formatter can help keep your stylesheets clean and readable—especially useful when you have multiple @font-face declarations and utility classes to manage. After finalizing your HTML font-loading markup, run it through the HTML Minifier to compress your <head> tag markup before deploying to production.

Common Mistakes to Avoid

Loading more than two typefaces. Every additional font family is another set of HTTP requests. Use weight variation within one or two families to create hierarchy instead.

Forgetting font-display in @font-face declarations. This causes the invisible text problem that makes users think the page is broken on slow connections.

Using px units exclusively for font sizes. This breaks browser text zoom for users with accessibility needs. Use rem for font sizes, em for component-relative spacing.

Preloading too many files. Preloading three or four font files competes with images, scripts, and other critical resources. Preload one—your body text font—and let everything else load normally.

Not testing on real devices. What looks fine on a fast desktop connection can cause significant FOUT on mobile. Test with Chrome DevTools' network throttling set to "Slow 3G" before shipping.

Next Steps

Now that you have a solid foundation for web fonts, dig into related CSS performance topics:

  • CSS Performance Guide — Learn how to audit and optimize your entire stylesheet, not just font loading
  • Core Web Vitals and CLS — Understand how font swapping affects your Cumulative Layout Shift score and what Google measures
  • CSS Formatter — Clean up and organize your CSS after adding font stacks and @font-face rules
  • HTML Minifier — Compress your HTML including font preload tags before deploying

For font resources, the Google Fonts website lets you preview combinations and generate the exact URL you need. Font Squirrel's Webfont Generator converts fonts to woff2 and handles subsetting for self-hosted setups. If you're looking for variable fonts specifically, v-fonts.com catalogs what's available and which axes each font supports.

Related Tools

Continue Learning