Web Fonts Guide: Google Fonts, Self-Hosting, and Performance
Learn how to use web fonts effectively while keeping your site fast and accessible.
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-facerules - 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
Web Typography Best Practices
Learn typography fundamentals for beautiful, readable web text.
intermediateWeb Performance Optimization
Learn to build fast-loading websites with modern performance techniques.
intermediateHTML Performance Optimization: Speed Up Your Website in 2026
Learn how to optimize HTML for faster page loads. Cover lazy loading, preloading, resource hints, and Core Web Vitals optimization.