Web Font Implementation Guide
Complete end-to-end checklist for deploying fonts on a production website. From selecting a font license to verifying Core Web Vitals, every step in order.
Phase 1 — Selection and licensing
- Choose your fonts — one heading font, one body font maximum. Prefer variable fonts when available.
- Verify the license — use the License Metadata Viewer to read the embedded license. Confirm commercial use, web embedding, and modification (subsetting) rights.
- Check language coverage — use the Language Support Checker if your site serves non-English content.
- Test rendering — use the Font Preview Generator to confirm the font renders your actual content correctly.
Phase 2 — File preparation
- Download source files — TTF or OTF from the font foundry.
- Subset if needed — use
pyftsubsetto strip unused scripts. See the Optimization Guide. - Convert to WOFF2 — use the TTF to WOFF2 Converter or OTF to WOFF2 Converter.
- Verify output — use the Performance Checker to confirm format and size.
- Place in project — put WOFF2 files in
/public/fonts/or equivalent.
Phase 3 — CSS and HTML integration
- Generate @font-face CSS — use the @font-face Generator. Copy the output into
fonts.css. - Generate fallback stack — use the Fallback Generator. Apply to
body. - Generate preload tag — use the Preload Generator. Add the tag to
<head>. - Verify integration — use the Integration Tester in a staging environment to confirm the font loads.
<head>
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/css/fonts.css">
<link rel="stylesheet" href="/css/main.css">
</head>
/* fonts.css */
@font-face {
font-family: "Main";
src: url("/fonts/main.woff2") format("woff2");
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
/* main.css */
body {
font-family: "Main", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
Phase 4 — Server configuration
- Set long cache headers for font files (one year, immutable).
- Set correct MIME types —
font/woff2for.woff2files. Most servers get this wrong by default. - Enable CORS for fonts if serving from a CDN or subdomain —
Access-Control-Allow-Origin: *. - Enable Brotli compression — serves .woff2 as-is (already compressed), but ensures your HTML/CSS/JS gets Brotli too.
Cloudflare Pages _headers example:
/fonts/*
Cache-Control: public, max-age=31536000, immutable
Access-Control-Allow-Origin: *
Content-Type: font/woff2
Phase 5 — Verification
- Check Core Web Vitals in Google Search Console after 28 days of real-user data. The report shows LCP, INP, and CLS at the 75th percentile.
- Test in real browsers — Chrome, Firefox, Safari, Edge. Use the Integration Tester on each.
- Test on mobile networks — throttle to Slow 3G in DevTools. Confirm fallback text is visible within 1s.
- Test with disabled cache — verify the font loads on a fresh visit.
- Test with fonts blocked — verify the fallback stack prevents invisible text.
Phase 6 — Ongoing maintenance
- Monitor Google Search Console Core Web Vitals monthly.
- Update fonts only when the design system changes — each update is a new cache entry.
- Review the fallback stack when adding new languages or scripts.
- Re-run the Performance Checker when adding new font files.
- Re-run the Integration Tester after any server or CDN configuration change.
Common mistakes to avoid
| Mistake | Impact | Fix |
|---|---|---|
Using @import for fonts | Blocks rendering — worst-case 1s+ LCP delay | Use <link> in <head> |
Missing crossorigin on preload | Double font download | Add crossorigin |
| Preloading 5+ fonts | Steals bandwidth from LCP-critical resources | Preload only the critical font |
| Serving TTF or OTF | 60–75% larger than WOFF2 | Convert to WOFF2 |
| Wrong MIME type | Some browsers ignore the font | Serve as font/woff2 |
| No fallback family | Text renders in unpredictable default | Always end with generic family |
| Loading 9 static weights | ~900KB for one family | Use a variable font (~100KB) |
Frequently asked questions
What is the recommended web font setup for production?
Self-host WOFF2 variable font files (or subsetted static weights), serve via @font-face with font-display: swap, preload the critical font with crossorigin, include a metric-adjusted fallback, and set 1-year immutable cache headers.
Do I need to serve WOFF, WOFF2, TTF, and OTF all together?
No. WOFF2 alone is sufficient for all browsers released after 2016. Serving multiple formats doubles or triples your deployment size without meaningful compatibility benefit for modern audiences.
How many fonts should I load?
One or two families maximum. Prefer one variable font if possible. Each additional family adds network requests, delays LCP, and increases visual noise.
Should I use Google Fonts or self-host?
Self-host for production. Google Fonts CDN is fine for prototypes but adds two DNS lookups, prevents stable preload URLs, and requires GDPR consent in the EU.
What if my fonts are on a CDN?
Ensure the CDN sets Access-Control-Allow-Origin headers for font files, uses font/woff2 MIME type, and serves with long cache headers. Preload URLs must match the CDN URL exactly.