Understand WOFF2 vs legacy formats, eliminate invisible text with font-display strategies, and use preload hints to move fonts off the critical path for instant rendering.
Fonts are the most common cause of both layout shift (CLS) and invisible text (FOIT) on the web. A developer who understands font formats, the browser loading pipeline, and the font-display descriptor can cut perceived load time by 30-50% and eliminate the jarring content shift that kills Lighthouse scores. This lesson gives you concrete, measurable control over how fonts arrive in the browser.
WOFF2 (Web Open Font Format 2) is the only format you need for modern browsers — 98%+ browser support, Brotli compression yields 30-50% smaller files than WOFF and 50-70% smaller than TTF. WOFF (version 1) uses zlib compression and covers IE9-11; include it only if you must support those browsers. TTF and OTF are uncompressed source formats — serve them only as a last resort since they bloat payloads by 3-5x. EOT was IE-only and is fully obsolete. Variable fonts ship as a single WOFF2 file encoding the full design space across weight, width, and italic axes. Although a single variable WOFF2 is larger than a single static WOFF2, loading one file instead of 6-8 weight files is a clear network win.
1/* 2024 setup: WOFF2 only for modern browsers */
2@font-face {
3 font-family: 'Inter';
4 src: url('/fonts/inter-var.woff2') format('woff2');
5 font-weight: 100 900; /* Variable font range */
6 font-style: normal;
7 font-display: swap;
8}
9
10/* Legacy fallback if IE11 must be supported */
11@font-face {
12 font-family: 'Inter';
13 src: url('/fonts/inter-400.woff2') format('woff2'),
14 url('/fonts/inter-400.woff') format('woff');
15 font-weight: 400;
16 font-style: normal;
17 font-display: swap;
18}FOIT (Flash of Invisible Text) is when the browser hides text until the custom font is ready. Users see blank white space where content should be — visually jarring and a Lighthouse performance hit. The default browser block period is 3 seconds; if the font loads within 3s, text appears. If it does not, the browser falls back. FOUT (Flash of Unstyled Text) is the opposite: the browser renders text in the fallback font immediately, then swaps to the custom font once loaded. This causes a visual jump — line lengths change, element heights shift, CLS increases. Neither is ideal. The art is choosing which trade-off fits the context: for body text, brief FOUT is far better than FOIT because users can read immediately. For brand display headings, a very brief FOIT (100ms) prevents a jarring visual swap. The font-display CSS descriptor gives you direct control over both periods.
The font-display descriptor inside @font-face sets the block period (FOIT duration) and swap period (FOUT window). auto: unpredictable browser-default — never use it in production. block: 3s FOIT then unlimited FOUT. Only appropriate for icon fonts where showing fallback characters would display wrong symbols. swap: 0ms FOIT, unlimited FOUT. Text visible immediately with system fallback, swaps when custom font loads. Best for body text and UI text. fallback: 100ms FOIT, 3s swap period. If font is not loaded within ~3s, browser permanently uses fallback for this page view (font cached for next visit). Good choice when combined with preloading. optional: 100ms FOIT, 0ms swap. If the font is not cached or does not load within 100ms, browser permanently uses fallback — no late swap, zero CLS. Best for non-critical decorative fonts.
1/* Body text: readable immediately is more important than visual consistency */
2@font-face {
3 font-family: 'Inter';
4 src: url('/fonts/inter.woff2') format('woff2');
5 font-weight: 400;
6 font-display: swap;
7}
8
9/* Display font: avoid jarring late swap on landing page hero */
10@font-face {
11 font-family: 'Playfair Display';
12 src: url('/fonts/playfair.woff2') format('woff2');
13 font-weight: 700;
14 /* With preload: font likely cached; optional prevents late swap */
15 font-display: optional;
16}Without a preload hint, fonts start downloading only after: HTML is fetched → CSS is fetched and parsed → CSSOM is built → browser finds a font-using CSS rule → browser finds a matching DOM node. This is typically 500-900ms after navigation starts. A <link rel='preload'> moves the font download to the very beginning of HTML parsing — saving 300-600ms. Rules: only preload fonts you know appear above the fold. Too many preloads compete with other critical resources. Always include as='font' and crossorigin (the browser performs an anonymous CORS fetch for fonts, even same-origin). Include type to allow format detection.
1<!-- In <head> before stylesheets — order matters -->
2<link
3 rel="preload"
4 href="/fonts/inter-var.woff2"
5 as="font"
6 type="font/woff2"
7 crossorigin
8/>
9<!-- Only preload 1-2 fonts maximum -->
10<link rel="stylesheet" href="/styles.css" />1/* Pair preload with font-display: optional for zero CLS */
2@font-face {
3 font-family: 'Inter';
4 src: url('/fonts/inter-var.woff2') format('woff2');
5 font-weight: 100 900;
6 /* optional: if preload succeeds, font is ready; if not, use fallback */
7 font-display: optional;
8}A full Latin font file contains glyphs for 800-1200 characters covering dozens of languages. If your content is English-only, you use perhaps 200-250 glyphs. The unicode-range descriptor tells the browser to only download a font file when it encounters a character in that Unicode range. Split one large file into several small subset files. The browser downloads only the subsets it actually needs. This is how Google Fonts achieves the small file sizes you see in the Network panel — they serve 40+ tiny subset files and only deliver the ones your page needs.
1/* Latin characters only — downloaded for most English pages */
2@font-face {
3 font-family: 'Inter';
4 src: url('/fonts/inter-latin.woff2') format('woff2');
5 font-weight: 400;
6 font-display: swap;
7 unicode-range: U+0000-00FF, U+0131, U+0152-0153,
8 U+02BB-02BC, U+02C6, U+02DA, U+02DC,
9 U+2000-206F, U+20AC, U+2122, U+FEFF, U+FFFD;
10}
11
12/* Latin Extended — only downloaded if those characters appear */
13@font-face {
14 font-family: 'Inter';
15 src: url('/fonts/inter-latin-ext.woff2') format('woff2');
16 font-weight: 400;
17 font-display: swap;
18 unicode-range: U+0100-024F, U+0259, U+1E00-1EFF,
19 U+2020, U+20A0-20AB, U+20AD-20CF;
20}Full HTML and CSS for a landing page with preloaded, subsetted variable font and a fallback that minimises CLS.
Next, you will learn how to build system font stacks and web-safe fallback chains that render instantly with no download, while still maintaining design quality.
Your progress is saved in this browser, on this device only.