Font Optimization: font-display, preload, subset
Custom fonts — common cause of slow FCP and CLS. Browser must load font before text display (or show invisible text / system font). Proper setup eliminates both symptoms.
font-display — rendering control
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap; /* show fallback immediately */
}
| Value | Behavior | When to use |
|---|---|---|
auto |
Browser dependent | Don't use |
block |
Hide text until load (FOIT) | Icon fonts |
swap |
Fallback → main when loaded | Body text |
fallback |
100ms hidden, then swap | Balance FCP/CLS |
optional |
Only if cached | Non-critical fonts |
For body text — swap. For decorative headings where FOUT is acceptable — swap. For icon fonts — block (else icons show as symbols).
Preload critical font
<head>
<!-- Preload before CSS loads -->
<link rel="preload"
href="/fonts/inter-regular.woff2"
as="font"
type="font/woff2"
crossorigin>
<!-- Multiple weights — preload only used above-the-fold -->
<link rel="preload" href="/fonts/inter-700.woff2" as="font" type="font/woff2" crossorigin>
</head>
Preload only first-screen fonts. Preloading all weights slows loading.
Subset — only needed characters
Full Inter contains Latin, Cyrillic, Greek, Hebrew — ~500 kB. Subset with only needed characters — 30–80 kB.
/* unicode-range for Cyrillic */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-cyrillic.woff2') format('woff2');
font-weight: 400;
font-display: swap;
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* Latin — separate file */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-latin.woff2') format('woff2');
font-weight: 400;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}
Browser loads only files with characters on the page.
Subset generation via pyftsubset
# Installation
pip install fonttools brotli
# Create subset with Cyrillic + basic Latin
pyftsubset inter-regular.ttf \
--unicodes="U+0020-007E,U+00A0-00FF,U+0400-045F,U+0490-0491" \
--layout-features="kern,liga,calt" \
--flavor=woff2 \
--output-file=inter-regular-subset.woff2
Self-hosted vs Google Fonts
Google Fonts introduces DNS lookup + connection overhead for each new user. Self-hosted fonts:
- Remove cross-origin delay
- Enable HTTP/2 push or preload
- Work without third-party CDN dependency
# Download font with proper subset
# Using google-webfonts-helper (gwfh.mranftl.com)
# or fontsource npm package
npm install @fontsource-variable/inter
/* Fontsource — automatically includes needed files */
@import '@fontsource-variable/inter/cyrillic.css';
@import '@fontsource-variable/inter/latin.css';
Compensating CLS from font-swap
/* Pick metrics for fallback font */
@font-face {
font-family: 'InterFallback';
src: local('Arial');
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
size-adjust: 107%;
}
body {
font-family: 'Inter', 'InterFallback', sans-serif;
}
Tools for value picking: fontaine npm package or next/font (automatically for Next.js).
Variable fonts
One file instead of separate for each weight:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-variable.woff2') format('woff2-variations');
font-weight: 100 900; /* range */
font-display: swap;
}
/* Usage */
h1 { font-weight: 700; }
body { font-weight: 400; }
.light { font-weight: 300; }
Variable font Inter (~170 kB subset Cyrillic+Latin) vs. separate files for 400 + 700 (~120 kB). Worth when ≥ 3 weights.
Optimization time: 4–8 hours: download fonts, make subset, setup preload and CSS.







