How to divide your font weight by 10 with subsetting
Most websites use custom fonts, often loaded from Google Fonts. As you probably know, you need to download as many files as you have weights. If you need 4 weights and their italic variants, that means 8 files (8 HTTP requests, 6 files of 150KB in TTF, etc.).
We'll see how to reduce the weight of your variable fonts by more than 90% (and improve your CLS (Cumulative Layout Shift)) while loading all weights in a single file.
We'll use Roboto as an example, and to compare performance, we'll convert the TTF files to WOFF2 to establish a baseline with optimized static fonts.
| Static font | TTF (source) | WOFF2 (converted) |
|---|---|---|
| Roboto-Regular | 156 KB | 68 KB |
| Roboto-Bold | 157 KB | 70 KB |
| Roboto-Light | 156 KB | 68 KB |
| Roboto-Black | 157 KB | 69 KB |
| Roboto-Italic | 162 KB | 74 KB |
| Roboto-BoldItalic | 163 KB | 76 KB |
| Roboto-LightItalic | 162 KB | 74 KB |
| Roboto-BlackItalic | 163 KB | 75 KB |
| TOTAL | 1.276 MB | 0.574 MB |
As you can see, font weight on a site is comparable to loading a dozen images.
Converting from TTF to WOFF2 brings a 55% gain — but it's not enough.
Let's see what we can achieve by switching to variable fonts instead.
What a variable font really is
A variable font is a single file that encodes multiple typographic variations along one or more continuous axes. The wght (weight) axis is the most common: it allows going from Thin (100) to Black (900) without downloading nine separate files.
Other axes exist: ital (italic), wdth (width), opsz (optical size), or proprietary axes. A font like Roboto Flex combines a dozen of them.
In practice, a variable TTF file contains:
- all glyphs for each point on the axis,
- variation tables (GVAR, HVAR, MVAR...),
- metadata for all named instances.
One file for all weights (and much more) — but at what cost?
| Variable font | TTF (source) | WOFF2 (converted) |
|---|---|---|
| Roboto-VariableFont_wdth,wght | 477 KB | 216 KB |
| roboto-italic-variablefont-wdth,wght | 518 KB | 249 KB |
| TOTAL | 0.995 MB | 0.465 MB |
The variable version lets us slightly reduce font weight when using 4 or more weights. In other cases, it's the opposite — the variable font cost isn't worth it.
The real problem: unused glyphs
A Google Fonts file often covers hundreds of writing systems. Roboto, for example, includes basic Latin, extended Latin, Cyrillic, Vietnamese, Greek… all glyphs that an English or French website has absolutely no use for.
Subsetting means extracting only the glyphs corresponding to the characters actually used. For an English or French site, the following Unicode ranges cover 99.9% of needs:
U+0020-007F— Basic Latin (ASCII): letters, digits, common punctuationU+00A0-00FF— Latin-1 Supplement: French accents, currency symbols (€, £), marks (©, ®, ™)U+0152-0153— A portion of Latin Extended-A: the "œ" and "Œ" ligatures only
Everything else? Removed. The gain is immediate and drastic.
| Variable font | TTF (source) | WOFF2 | WOFF2 Latin |
|---|---|---|---|
| Roboto-VariableFont_wdth,wght | 477 KB | 216 KB | 77 KB |
| roboto-italic-variablefont-wdth,wght | 518 KB | 249 KB | 89 KB |
| TOTAL | 0.995 MB | 0.465 MB | 0.166 MB |
Comparison with popular Google Fonts
Let's now compare the results across a sample of 5 popular variable Google Fonts.
| Font | Variable TTF (source) | Full WOFF2 | Latin WOFF2 | Gain |
|---|---|---|---|---|
| Roboto | 477 KB | 217 KB | 77 KB | -84% |
| Open Sans | 518 KB | 274 KB | 83 KB | -84% |
| Inter | 855 KB | 341 KB | 85 KB | -90% |
| Montserrat | 673 KB | 202 KB | 51 KB | -92% |
| Roboto Flex | 1646 KB | 730 KB | 288 KB | -82% |
Download optimized fonts
Here are a few fonts I've already slimmed down, ready to download and use on your own sites:
These ultra-optimized versions will speed up your site's loading time and improve your CLS (Cumulative Layout Shift).
Create your own font subsets
The reference tool is fontTools, the Python library maintained by Google and used internally to produce the Google Fonts themselves.
Installation:
sudo apt install python3-fonttools python3-brotli
or
pip3 install fonttools brotli
The subset command, as used in this project:
python3 -m fontTools.subset "resources/fonts/Lexend-VariableFont_wght.ttf" \
--unicodes="U+0020-007F,U+00A0-00FF,U+0152-0153" \
--flavor=woff2 \
--output-file="public/fonts/lexend-variablefont-wght.woff2" \
--layout-features="*" \
--name-IDs="*"
What each option does:
--unicodes: the character ranges to keep (everything else is discarded)--flavor=woff2: WOFF2 compression directly at output, no intermediate step needed--layout-features="*": preserves all OpenType features (ligatures, kerning, etc.)--name-IDs="*": preserves font metadata (required for named instances)
Automated in a makefile, this gives a font-subset target called at build time. A single call to process all fonts in the resources/fonts/ folder:
font-subset: ## Generate a woff2 subset for each font in resources/fonts/ to public/fonts/
@for font in resources/fonts/*; do \
name=$$(basename "$$font" | sed 's/\.[^.]*$$//' | sed 's/_/-/g' | tr '[:upper:]' '[:lower:]'); \
output="public/fonts/$$name.woff2"; \
python3 -m fontTools.subset "$$font" \
--unicodes="U+0020-007F,U+00A0-00FF" \
--flavor=woff2 \
--output-file="$$output" \
--layout-features="*" \
--name-IDs="*"; \
done
Optimize your @font-face declaration
Now that the file is optimized, we can also optimize its loading in the @font-face declaration to improve perceived performance.
<!-- Preload: the browser downloads the font as soon as the <head> is parsed -->
<link rel="preload" href="/fonts/lexend-variablefont-wght.woff2" as="font" type="font/woff2" crossorigin>
<style>
@font-face {
font-family: 'Lexend';
src: url('/fonts/lexend-variablefont-wght.woff2') format('woff2');
font-weight: 100 900; /* all weights available on the wght axis */
font-style: normal;
font-display: optional; /* abandonne après > 100ms */
}
/* Calibrated fallback for zero layout shift */
@font-face {
font-family: 'Lexend Fallback';
src: local('Arial'); /* display a calibrated Arial while waiting for Lexend */
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
size-adjust: 107%;
}
</style>
Time to clean up your fonts!
The recipe is simple:
- A variable TTF font in
resources/fonts/ - A
make font-subsetat build time with fontTools → subsetted WOFF2 file inpublic/fonts/ - A properly declared
@font-facewithfont-weight: 100 900andfont-display: optional - A calibrated fallback with
ascent-override,descent-overrideandsize-adjust
The result on uxcode : Lexend at 28 KB, loaded in a single request, with all weights available from 100 to 900, no typographic flash, no layout shift.
The outcome? Rich typography, a single request under 30 KB, and one more best practice toward a Lighthouse score aiming for 100%.
This is the standard approach for any serious web project in 2026.