Font Optimization: Subsetting, Variable Fonts and font-display
Web fonts are often the overlooked factor in performance optimizations. A site that uses Google Fonts without optimization can load between 300KB and 800KB fonts only — resources that block rendering, impact LCP, and cause layout shift during font swap. With the right techniques, that same typography can require less than 60KB and do not cause any visible shifts.
The web font market has changed radically in recent years. THE variable
fonts allow you to include all variants (regular, bold, italic) in
a single file instead of four or more separate files. Subsetting reduces files
removing unnecessary glyphs. And the strategies font-display allow
to control exactly how your browser behaves when loading.
What You Will Learn
- How WOFF2 works and why it is the optimal format for the web
- Font subsetting: reduce files from 400KB to 60KB with pyftsubset and Glyphhanger
- Variable fonts: one file for all variations, with savings of 40-60%
- The font-display strategies: swap, optional, fallback, block
- unicode-range to load only the necessary subsets
- Preconnect and preload to eliminate FOIT (Flash of Invisible Text)
- Override metrics (@font-face) to reduce CLS from font swap
WOFF2: The Standard Format for Web Fonts
WOFF2 (Web Open Font Format 2) uses Brotli compression, resulting in a reduction 30-50% compared to WOFF and 60-70% compared to original OTF/TTF formats. Browser support is universal (100% of modern browsers) so it is no longer necessary provide WOFF or TTF fallbacks for sites targeting browsers from the last 5 years.
/* Configurazione @font-face moderna: solo WOFF2 */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
/* Variable font: un unico file per tutti i pesi */
@font-face {
font-family: 'Inter Variable';
src: url('/fonts/inter-variable.woff2') format('woff2-variations');
font-weight: 100 900; /* range continuo */
font-style: normal;
font-display: swap;
}
Font Subsetting: Reduce the File to the Necessary Minimum
A complete font file like Inter Regular contains approximately 2,400 covering glyphs hundreds of languages. If your site is only in Italian and English, you need less than 300 glyphs. Subsetting removes all unused glyphs, reducing the file size drastically.
Subsetting with pyftsubset (Python)
# Installa fonttools
pip install fonttools brotli
# Subset per Latin Extended (caratteri italiani inclusi)
pyftsubset Inter-Regular.ttf \
--output-file=inter-regular-subset.woff2 \
--flavor=woff2 \
--unicodes="U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, \
U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, \
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
# Risultato tipico:
# Inter-Regular.ttf: 1.2MB (OTF originale)
# inter-regular-subset.woff2: ~52KB (riduzione del 95%)
Glyphhanger: Analysis and Automatic Subsetting
Glyphhanger scans your site (or static HTML) and generates the minimum subset based on the characters actually used:
# Installa
npm install -g glyphhanger
# Scansiona un sito e genera il subset per i font usati
glyphhanger https://tuosito.com --subset=/fonts/*.ttf --formats=woff2 --LATIN
# Scansiona file HTML locali
glyphhanger ./dist/**/*.html --subset=/fonts/*.ttf --formats=woff2
unicode-range: Conditional Subsets
The property unicode-range in @font-face instructs the
browser to download that font file only if the page contains fonts in that
unicode range. This is the mechanism used by Google Fonts to serve subsets
different to users with different languages:
/* Subset Latin di base - scaricato per tutti */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-latin.woff2') format('woff2');
font-weight: 400;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074,
U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
/* Subset Cirillico - scaricato solo se la pagina usa cirillico */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-cyrillic.woff2') format('woff2');
font-weight: 400;
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
Variable Fonts: One File for All Variations
Variable fonts (introduced in the OpenType 1.8 specification) allow a single file to contain a continuum of variants along one or more axes of variation. The most common axes are:
- wght (weight): 100 to 900
- wdth (width): condensed, normal, expanded
- ital (italic): from upright to italic
- slnt (slant): slant of the character
- oops (optical size): optimization for different sizes
/* Variable font con asse weight */
@font-face {
font-family: 'Inter Variable';
src: url('/fonts/inter-variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
/* Uso di font-variation-settings per control granulare */
h1 {
font-family: 'Inter Variable', sans-serif;
font-weight: 700;
/* Oppure usando direttamente variation settings */
font-variation-settings: 'wght' 700, 'opsz' 32;
}
p {
font-family: 'Inter Variable', sans-serif;
font-weight: 400;
font-variation-settings: 'wght' 400, 'opsz' 16;
}
/* Animazione fluida del peso con variable fonts */
.title-animated {
font-family: 'Inter Variable', sans-serif;
transition: font-variation-settings 0.3s ease;
}
.title-animated:hover {
font-variation-settings: 'wght' 800;
}
Real Savings with Variable Fonts
| Scenario | Traditional Approach | Variable Font | Savings |
|---|---|---|---|
| 4 weights (400,500,600,700) | 4 files × 30KB = 120KB | 1 file = 48KB | 60% |
| 4 weights + italic | 8 files × 30KB = 240KB | 1 file = 72KB | 70% |
| Full display font | 12 files = 360KB | 1-2 files = 90KB | 75% |
The font-display strategies
The property font-display controls how the browser handles the
text display while loading web font. The choice of
right strategy depends on the trade-off between FOIT (Flash of Invisible Text) and
FOUT (Flash of Unstyled Text), and their impact on CLS and LCP.
font-display: swap (most common)
The browser immediately displays the text with the system font (FOUT), then replaces it with the web font when it is available. No FOIT, but potential CLS if fonts they have different metrics. The right choice for most sites.
font-display: optional (for maximum performance)
The browser displays text in system fonts. The web font is downloaded to background but used only if available within a very short period (about 100ms). If the connection is slow, the system font is used forever during that connection session. No FOUT, no CLS, but the web font may never be shown on slow connections. Ideal for sites where performance exceeds aesthetics.
font-display: fallback (the compromise)
The browser has a very short blocking period (about 100ms) then shows the fallback, and replaces with the web font only if available within 3 seconds. After 3 seconds, the system font remains throughout the session. A good compromise between FOIT and FOUT for fonts that have a strong impact on the visual identity.
Visual Comparison of Strategies
| Strategy | Block Period | Swap Period | FOIT | FOUT/CLS |
|---|---|---|---|---|
| block | 3s | infinite | High | Si |
| swap | 0ms | infinite | No | Si |
| fallback | 100ms | 3s | Minimum | Limited |
| optional | 100ms | 0ms | Minimum | No |
| car | browser decides | browser decides | Variable | Variable |
Eliminate CLS Font Swap with Override Metrics
The most effective technique to eliminate CLS caused by font-display: swap
and align the fallback font metrics with those of the web font. CSS properties
size-adjust, ascent-override, descent-override
e line-gap-override allow you to do this with millimeter precision.
/* Trova i valori corretti per Inter su Arial come fallback */
/* Metodo 1: Usa lo strumento Fallback Font Generator di Malte Ubl */
/* https://screenspan.net/fallback */
/* Metodo 2: Calcola manualmente le metriche */
/* Inter: UPM 2048, ascender 1984, descender -494, line-gap 0 */
/* Arial: UPM 2048, ascender 1854, descender -434, line-gap 67 */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
/* size-adjust: rapporto tra le dimensioni complessive */
size-adjust: 107.64%;
/* Override delle metriche verticali */
ascent-override: 90.2%;
descent-override: 22.48%;
line-gap-override: 0%;
}
/* Usa sempre il fallback ottimizzato come secondo nella stack */
body {
font-family: 'Inter', 'Inter Fallback', Arial, sans-serif;
}
Preconnect and Preload for Third Party Fonts
If you use Google Fonts or other third-party font CDNs, DNS and TLS connections
towards those domains they add latency. The directives preconnect e
preload in the <head> reduce this overhead:
<!-- Per Google Fonts: preconnect ai domini necessari -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap"
rel="stylesheet">
<!-- Ancora meglio: self-hosting con preload diretto -->
<link
rel="preload"
href="/fonts/inter-variable.woff2"
as="font"
type="font/woff2"
crossorigin
>
Warning: Do not abuse the Preload
Preload tells the browser to load that resource with high priority, competing with critical CSS and JavaScript resources. Use preload only for main font above-the-fold — typically the font of the body text in the regular weight. If you preload too many fonts, you can degrade performance general of the page instead of improving them.
Self-Hosting vs Google Fonts: The Comparison
The choice between self-hosting and Google Fonts depends on several factors. Self-hosting offers more control, better performance and no dependency on third-party services. Google Fonts offers convenience and shared caching between sites (although the latter benefit and diminished with modern browser privacy restrictions).
For maximum performance, self-hosting with variable fonts and custom subsetting and the optimal choice. For speed of implementation without sacrificing too much performance, Google Fonts with correct preconnect directives and acceptable.
/* Pipeline completa per self-hosting ottimizzato:
1. Scarica i font originali (OTF/TTF) dal sito del font designer
2. Crea subset con pyftsubset per Latin + caratteri speciali necessari
3. Converti in WOFF2
4. Carica i file in /public/fonts/
5. Configura gli header HTTP per il caching long-term
*/
/* Nel server (es: nginx) */
location ~* \.(woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin "*";
}
Next Steps
With font optimization mastered, you're ready to explore what's new in CSS that are changing the way we build responsive components. Next article in the series delves into CSS Container Queries: how to build components that react to the size of their container instead of the viewport, making the pattern with responsive utility classes obsolete.
Conclusions
The optimization of web fonts is one of the interventions with the highest impact/ effort in performance optimizations. Start with 400KB non-optimized fonts and get to subset variable font of 60KB with override metrics to eliminate the CLS requires a few one-off hours of work but has permanent benefits over LCP, CLS and bandwidth for each visitor.
The recommended strategy in 2026: self-hosting with variable fonts in WOFF2,
subsetting for Latin Extended, font-display: swap with override metrics
for the fallback font, e <link rel="preload"> for the font of the
body of the text. This setup achieves native performance without aesthetic compromises.







