
Web fonts enhance brand identity and design consistency, but they can also be a performance bottleneck. This guide shows you how to deliver stunning typography while keeping your site fast and responsive.
Typical webfont files range from 20–200 KB each. A site loading 4–6 font files can easily add 500 KB+ to the page weight, delaying text rendering and causing layout shifts. The key is strategic optimization.
Each weight and style is a separate file. Be ruthless:
/* Bad: loading 8 files */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
/* Good: loading 2–3 files */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
Rule of thumb: Use 2–3 weights maximum. Most sites only need regular (400), semi-bold (600), and bold (700).
WOFF2 offers 30% better compression than WOFF and 50%+ better than TTF/OTF:
@font-face {
font-family: "Inter";
src: url("/fonts/inter.woff2") format("woff2");
/* Fallback for older browsers */
src: url("/fonts/inter.woff") format("woff");
}
Browser support: WOFF2 works in all modern browsers (95%+ coverage).
Remove unused characters to dramatically reduce file size:
/* Latin subset only */
@font-face {
font-family: "Inter";
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+2000-206F, U+2074, U+20AC,
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
Tools for subsetting:
Preload fonts used above the fold to eliminate render-blocking:
<link rel="preload" as="font" type="font/woff2"
href="/fonts/inter-regular.woff2" crossorigin>
Warning: Only preload 1–2 critical fonts. Over-preloading delays other resources.
Control how text renders while fonts load:
@font-face {
font-family: "Inter";
src: url("/fonts/inter.woff2") format("woff2");
font-display: swap; /* Show fallback immediately */
}
Options:
swap: Show fallback text immediately, swap when font loads (best for most sites)optional: Use font only if cached; otherwise use fallback (best for performance)fallback: Brief invisible period, then swap (compromise)block: Hide text until font loads (avoid this)Self-hosting gives you control over caching, compression, and delivery:
Benefits:
Drawback: You manage updates and hosting
Replace multiple static files with one variable font:
/* Before: 6 files (light, regular, medium, semibold, bold, black) */
/* After: 1 variable font file */
@font-face {
font-family: "Inter";
src: url("/fonts/inter-var.woff2") format("woff2");
font-weight: 100 900; /* Full weight range */
}
Savings: Often 40–60% smaller than loading multiple weights.
<!-- Establish early connection to font CDN -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- DNS lookup only (lighter than preconnect) -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
Fine-grained control over font loading:
const font = new FontFace('Inter', 'url(/fonts/inter.woff2)');
font.load().then(() => {
document.fonts.add(font);
document.body.classList.add('fonts-loaded');
});
Inline font-face declarations in <head> to eliminate render-blocking requests:
<style>
@font-face {
font-family: "Inter";
src: url("/fonts/inter.woff2") format("woff2");
font-display: swap;
}
</style>
If using Google Fonts, optimize the URL:
<!-- Bad: loading too many weights -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<!-- Good: minimal weights with display=swap -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<!-- Better: add preconnect -->
<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;600;700&display=swap" rel="stylesheet">
font-display: swap or optionalPhoto by Matt Weissinger on Pexels