From harness-claude
Optimizes web font loading strategies: font-display to avoid FOUT/FOIT, variable fonts, subsetting, system stacks, self-hosting vs CDNs. Reduces LCP/CLS.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Font loading strategy — performance vs. FOUT/FOIT, variable fonts, subsetting, system font stacks, and font-display options
Optimizes web font loading using font-display for FOIT/FOUT control, preloading, Unicode subsetting, variable fonts, WOFF2 compression, and self-hosting vs CDN to reduce CLS and improve LCP.
Browses Google Fonts via browser automation to select typography pairings for frontend designs. Generates HTML imports, Tailwind configs, and curated fallbacks.
Guides web typography: font selection, pairing, scaling, spacing, responsive type, variable fonts, loading optimization, accessibility, and readability.
Share bugs, ideas, or general feedback.
Font loading strategy — performance vs. FOUT/FOIT, variable fonts, subsetting, system font stacks, and font-display options
Choose a font-display strategy. The font-display CSS descriptor controls what happens while a web font is loading. This is the single most impactful decision for font-related performance:
| Value | Behavior | Block Period | Swap Period | Best For |
|---|---|---|---|---|
swap | Shows fallback immediately, swaps when loaded | ~100ms | Infinite | Body text — user sees content instantly |
optional | Shows fallback; may never swap if load is slow | ~100ms | None | Performance-critical sites — no layout shift |
fallback | Brief invisible, then fallback, limited swap window | ~100ms | ~3s | Balance between swap and optional |
block | Invisible text for up to 3 seconds | ~3s | Infinite | Icon fonts only (never for text) |
auto | Browser decides (usually block) | Browser-dependent | Browser-dependent | Never use — behavior is unpredictable |
Decision procedure:
swap (guarantees content is always visible)optional (prevents layout shift; font loads from cache on subsequent visits)block (invisible squares are better than showing raw ligature text)auto or omit font-display — the browser default is effectively block, creating 3 seconds of invisible textUnderstand FOUT and FOIT. These are the two failure modes of web font loading:
font-display: block or browser default. Users see a blank page or missing content for up to 3 seconds. This is the worse failure mode — it hides content.font-display: swap. Users see a brief reflow. This is the better failure mode — content is always visible.swap) and minimize its visual impact by choosing a fallback font with similar metrics to your web font.Use WOFF2 exclusively for modern browsers. Font file formats in order of preference:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
}
Subset fonts aggressively. Most projects use only Latin characters. Loading a full Unicode font wastes bandwidth:
unicode-range in @font-face to load character sets on demand:/* Latin characters 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+0304, U+0308,
U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
font-display: swap;
}
Tools: glyphhanger (npm) analyzes which characters your site actually uses. pyftsubset (Python fonttools) creates subsets from any font file.
Evaluate variable fonts for multi-weight systems. A variable font contains all weights (and potentially widths, optical sizes) in a single file. The tradeoff:
Preload critical fonts. Add <link rel="preload"> for fonts that appear above the fold:
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin />
crossorigin attribute is required even for same-origin fonts (browser specification requirement)Build a system font stack as baseline or permanent solution. System fonts load in 0ms — they are already on the user's device:
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
'Helvetica Neue', sans-serif;
Platform resolution:
-apple-system)System font stacks are not a compromise — GitHub uses system fonts for their entire application UI. The only drawback is loss of brand-specific typography.
Self-hosting (recommended for most projects):
Google Fonts CDN:
<link href="https://fonts.googleapis.com/css2?...">)Next.js provides automatic font optimization via next/font:
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});
This automatically:
@font-face with font-display: swapA practical budget for font loading:
| Category | Budget | Example |
|---|---|---|
| Total font payload | < 100KB | 1 variable font (80KB) + 1 monospace (20KB) |
| Per-font file | < 50KB | Single static weight: ~20KB; Variable: ~80-100KB |
| Critical (above fold) | < 50KB | Preloaded primary font only |
| Fonts per page | 1-3 files | Primary text + optional heading + optional mono |
If your font payload exceeds 200KB, you are loading too many families, weights, or character sets. Audit with Chrome DevTools Network panel filtered to "Font."
To minimize FOUT layout shift, configure the fallback font to match the web font's metrics:
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
ascent-override: 90.49%;
descent-override: 22.56%;
line-gap-override: 0%;
size-adjust: 107.64%;
}
body {
font-family: 'Inter', 'Inter Fallback', sans-serif;
}
The size-adjust, ascent-override, descent-override, and line-gap-override descriptors align the fallback font's metrics to the web font, reducing visual shift when the swap occurs. Tools like fontaine (npm) and next/font calculate these values automatically.
Loading 6+ font files on initial page load. Each font file is a network request that blocks text rendering. Loading Regular, Italic, Bold, Bold Italic, Light, and Medium variants separately produces 6 requests and 120-180KB of font data. Fix: use a single variable font file, or limit to 2 static weights maximum.
No fallback font specified. A font-family: 'CustomBrandFont' declaration with no fallback produces invisible text if the font fails to load, or defaults to the browser's serif (Times New Roman on most systems) which likely has completely different proportions. Always provide a complete fallback stack.
Using font-display: block for body text. This hides all body text for up to 3 seconds while the font loads. On slow connections (3G mobile), users see a blank content area. The font may never load if the connection drops. Fix: use font-display: swap for all text content.
Loading full character sets for Latin-only content. A full Unicode build of Inter includes Greek, Cyrillic, Vietnamese, and other scripts. If your content is English-only, 90% of the font file is unused data. Fix: subset to Latin Basic (U+0000-00FF) plus any accent characters your content uses.
Google Fonts CDN without privacy audit. Google Fonts transmits the user's IP address to Google servers. In the EU, this has been ruled a GDPR violation (Munich Regional Court, January 2022). Even outside the EU, the unnecessary data transmission is a liability. Fix: download fonts and self-host them.
GitHub's System Font Strategy GitHub uses system fonts exclusively for the application UI:
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji';
Result: zero font-loading overhead, zero FOUT/FOIT, instant text rendering. Font payload: 0KB. GitHub reserves custom fonts (Mona Sans, Hubot Sans) only for marketing pages where brand expression justifies the performance cost.
Vercel's Geist Font Implementation
Vercel self-hosts Geist as a variable font via next/font:
<link rel="preload">font-display: swap for immediate text visibilityStripe's Font Loading Stripe loads sohne-var (variable) self-hosted:
font-display: swap with carefully tuned fallback metrics to minimize CLSfont-display descriptor, @font-face at-rulefont-display values, and identify whether subsetting is applied. Run Lighthouse to check CLS from font swap.font-display: swap for text, preload critical fonts, self-host rather than CDN. Consider variable fonts if using 3+ weights.This is a knowledge skill. When activated, it provides font loading performance expertise to guide @font-face declarations, preload hints, and font file optimization. Use these principles when configuring next/font, adding custom fonts to any web framework, or auditing Core Web Vitals related to font loading. Cross-reference with design-typography-fundamentals for font selection and design-responsive-type for viewport-adaptive type.
font-display: swap or font-display: optional