Web Fonts — @font-face, Variable Fonts & Performance Guide
In this tutorial, you'll learn about Web Fonts. We cover key concepts, practical examples, and best practices.
Web fonts let designers use custom typefaces on the web — beyond the system font stack — but they come with a performance cost that must be managed carefully.
What You'll Learn
How to declare custom fonts with @font-face, use variable fonts for multiple weights from one file, optimize font loading with font-display and preloading, subset fonts to reduce file size, and avoid layout shift from font swaps.
Why It Matters
Typography is 95% of web design. Custom fonts define brand identity and improve readability, but poorly loaded fonts cause visible text swaps, layout shifts (CLS), and slow first paint. Doda Browser's font loading pipeline supports font-display negotiation, prioritizing fast text rendering while maintaining typographic quality.
Real-world use: DodaTech's tutorial site uses a single variable font file (Inter) that replaces 12 separate weight files — saving 85KB. The font-display: swap strategy shows system text immediately, replaced by the custom font once loaded, preventing invisible text.
flowchart LR A[Custom Font] --> B[Font Loading] B --> C[font-display: swap] B --> D[Preload] B --> E[Subsetting] C --> F[FOUT - Flash of Unstyled Text] C --> G[FOIT - Flash of Invisible Text] D --> H[Faster Discovery] E --> I[Smaller Files] style A fill:#4af,color:#fff
@font-face Declaration
The @font-face rule tells the browser about a custom font:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC;
}
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var-italic.woff2') format('woff2');
font-weight: 100 900;
font-style: italic;
font-display: swap;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
Font Format Support
| Format | Browser Support | Compression |
|---|---|---|
| WOFF2 | All modern browsers | Best (30-50% smaller than WOFF) |
| WOFF | All browsers | Good |
| TTF/OTF | Legacy | None |
| EOT | IE only | Windows-specific |
Always use WOFF2 with WOFF fallback:
@font-face {
font-family: 'Custom';
src: url('/fonts/custom.woff2') format('woff2'),
url('/fonts/custom.woff') format('woff');
font-display: swap;
}
Variable Fonts
A single file contains multiple weights, widths, and styles:
@font-face {
font-family: 'Inter-Variable';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900;
font-stretch: 75% 125%;
font-display: swap;
}
h1 {
font-family: 'Inter-Variable', sans-serif;
font-weight: 700;
font-stretch: 110%;
}
p {
font-family: 'Inter-Variable', sans-serif;
font-weight: 400;
}
.animated-weight {
font-family: 'Inter-Variable', sans-serif;
transition: font-weight 0.3s ease;
}
.animated-weight:hover {
font-weight: 800;
}
Font Loading Strategies
font-display Options
@font-face {
font-family: 'DisplayFont';
src: url('/fonts/display.woff2') format('woff2');
font-display: swap; /* Show fallback immediately */
}
@font-face {
font-family: 'BodyFont';
src: url('/fonts/body.woff2') format('woff2');
font-display: optional; /* Only use if already loaded */
}
@font-face {
font-family: 'IconFont';
src: url('/fonts/icons.woff2') format('woff2');
font-display: block; /* Short invisible period */
}
font-display |
Behavior |
|---|---|
auto |
Browser default (usually block) |
block |
Short invisible period, then swap |
swap |
Show fallback immediately, swap when loaded |
fallback |
Short invisible period, swap if loaded quickly |
optional |
Show fallback, use custom font only if cached |
Preloading Critical Fonts
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/heading-bold.woff2" as="font" type="font/woff2" crossorigin>
Font Loading API
const font = new FontFace('Dynamic', 'url(/fonts/dynamic.woff2)', {
weight: '400',
style: 'normal'
});
font.load().then(() => {
document.fonts.add(font);
document.body.style.fontFamily = 'Dynamic, sans-serif';
}).catch(err => {
console.warn('Font failed to load:', err);
});
Subsetting
Remove unused characters to shrink font files:
/* Latin-only subset excludes Cyrillic, Greek, CJK */
@font-face {
font-family: 'Subset';
src: url('/fonts/subset-latin.woff2') format('woff2');
unicode-range: U+0000-00FF; /* Basic Latin */
}
Tools like glyphhanger or fonttools subset fonts automatically. A full Inter Regular WOFF2 is ~300KB. A Latin-only subset is ~35KB.
Common Errors
- Not using WOFF2 — WOFF2 compression is 30-50% better than WOFF. Always serve WOFF2 with WOFF fallback for older browsers.
- Forgetting
crossoriginon preload — Fonts are fetched from a different origin (CDN).<link rel="preload">requirescrossoriginattribute or the font is loaded twice. - Loading all weights as separate files — Use variable fonts to get 10+ weights from one file. If variable fonts aren't an option, use
font-display: swapand preload the most common weight. - Not subsetting for Latin-only sites — Full fonts include Cyrillic, Greek, and other scripts you probably don't use. Subsetting reduces file size by 60-80%.
- Causing layout shift from font swap — Different fonts have different metrics. Use
font-size-adjustor reserve space withsize-adjustin@font-face(CSS Fonts Level 5).
Practice Questions
- What is the difference between
font-display: swapandfont-display: optional?swapalways uses the custom font eventually.optionalonly uses it if already cached, preventing any font swap. - What are variable fonts and why are they beneficial? One file contains multiple weights, widths, and styles. Benefits: fewer HTTP requests, smaller total size, and smooth interpolation between weights.
- How does preloading fonts improve performance? The browser discovers the font earlier in the loading process, before the CSS is parsed, reducing the time until the font is available.
- What is subsetting and why is it important? Removing unused character sets (scripts, symbols) from the font file. For English sites, removing Cyrillic and CJK reduces file size by 60-80%.
- Why does
font-display: swapcause layout shift? The fallback and custom fonts have different metrics (x-height, advance width). When the font swaps, text reflows. Usesize-adjustto normalize metrics.
Challenge
Build a font loading strategy for a bilingual site (English + Japanese). Preload the Latin variable font and the Japanese font. Use font-display: swap for body text and font-display: optional for decorative fonts. Subset the Latin font to Basic Latin only. Measure CLS before and after.
Real-World Task
Optimize the DodaTech tutorials site font loading. Convert all separate weight files to a single variable font. Subset for Latin-only pages. Preload the font in the <head>. Use font-display: swap with size-adjust to prevent layout shift. Verify CLS stays under 0.1 using Lighthouse.
Previous: Browser Storage Guide | Next: Image Optimization Guide | Related: Web Performance Optimization
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro