How to Subset Web Fonts with unicode-range
This guide belongs to Font Loading & Text Rendering Performance within Core Web Vitals & Measurement. A full-featured web font — Latin, extended Latin, Cyrillic, Greek, Vietnamese, plus OpenType features — can weigh 150–400KB per weight. Most pages render text from a few hundred glyphs. Every unused glyph is bytes on the critical path to your text LCP, competing with CSS and the hero image for bandwidth.
Subsetting removes glyphs you do not need. unicode-range in @font-face goes further: it lets you split a font into several files by script, and the browser downloads only the files whose ranges match characters actually present on the page. An English page downloads the Latin subset; the same stylesheet on a Ukrainian page downloads the Cyrillic one. Done well, the font that blocks your heading drops from 120KB to 18KB.
Rapid Diagnosis
- List font requests and sizes. In DevTools Network, filter by "Font". Note each file's transfer size and whether it finishes before your LCP.
- Check for multiple scripts in one file. Open the font in a tool like FontDrop or
fc-query; if it covers Cyrillic, Greek and Vietnamese but your site is English-only, it is oversized. - Check existing
unicode-rangedeclarations. Without them, any@font-faceused on the page is downloaded regardless of characters. - Look at OpenType features. Large fonts carry ligatures, alternates and kerning tables for many scripts; you may need some features (kerning, standard ligatures) but not all.
Root Cause Analysis
1. Shipping the vendor's full file. Fonts downloaded from foundries or font repositories default to full character sets.
2. One file for all languages. Multilingual sites serve one large font to every visitor instead of per-script subsets.
3. Too many weights and styles. Four weights × two styles × full character sets multiplies the problem; subsetting helps each file but the count still matters.
4. Subsetting without a fallback plan. Aggressive subsetting that drops characters used by user-generated content (names, quotes, emoji) leaves gaps rendered in fallback fonts.
Step-by-Step Resolution
1. Subset to the scripts you serve
pip install fonttools brotli
pyftsubset Inter-Bold.ttf \
--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" \
--layout-features="kern,liga,calt" \
--flavor=woff2 --output-file=inter-bold-latin.woff2
# trade-off: dropping layout features (e.g. tnum, ss01) removes them from the
# font entirely. Keep any feature your CSS uses via font-feature-settings or
# font-variant-numeric, or numbers will silently change shape.
Expected outcome: a Latin subset typically 60–80% smaller than the full file.
2. Split by script and declare unicode-range
@font-face {
font-family: "Inter";
src: url("/fonts/inter-bold-latin.woff2") format("woff2");
font-weight: 700; font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2000-206F, U+20AC, U+2122;
}
@font-face {
font-family: "Inter";
src: url("/fonts/inter-bold-cyrillic.woff2") format("woff2");
font-weight: 700; font-display: swap;
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* trade-off: a page mixing scripts (an English page with one Cyrillic name)
downloads both files. That is correct behaviour, but it means rare
characters can trigger a second font download late in the load. */
Expected outcome: each page downloads only the subsets its text requires.
3. Preload only the subset the LCP text needs
<link rel="preload" href="/fonts/inter-bold-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- trade-off: preload ignores unicode-range — the file is fetched whether or
not the page uses those characters. Preload only the primary-script subset
of the weight used by your LCP heading. -->
Expected outcome: the heading's font arrives in parallel with CSS, as described in preloading web fonts with crossorigin.
4. Guard against missing glyphs in user content
For user-generated text, keep a broader subset available (declared with its own unicode-range, not preloaded) so unusual characters render in your font when they appear, rather than falling back mid-word.
Expected outcome: no visible font mixing in names and quotes, with no cost to pages that do not need the extra characters.
Verification
In the Network panel, confirm that only the expected subset files are requested on representative pages in each language, and that their sizes match the build output. Use the Rendering drawer's "Disable local fonts" option to make sure you are seeing the web font, then scan pages for fallback-font characters. Compare LCP for text-LCP templates before and after; reductions of 100–400ms on mobile are typical when the font was on the critical path.
Subsetting Variable Fonts and Icon Fonts
Variable fonts can be subset by character and by axis range. If you only use weights 400–700 of a 100–900 variable font, fonttools varLib.instancer can restrict the weight axis, shrinking the file further before character subsetting. Icon fonts are a special case: they are entirely private-use glyphs, so subsetting means keeping only the icons you use — but the better fix is usually to replace them with inline SVG, covered in migrating from icon fonts to SVG. Either way, automate subsetting in the build so a designer adding a new character or icon does not require a manual step that someone forgets.
Common Subsetting Mistakes
- Subsetting away punctuation and symbols. Curly quotes, en and em dashes, ellipses, the euro and pound signs and non-breaking spaces live outside Basic Latin. Missing them makes those characters render in a fallback font mid-sentence.
- Mismatched ranges between CSS and files. Hand-maintained
unicode-rangelists drift from the subset files, causing characters to be "claimed" by a file that does not contain them. Generate both from one definition. - Dropping kerning. Removing the
kernfeature saves little and makes headings visibly uneven. Keep it. - Subsetting every weight identically. Display weights used only in headings may need fewer characters than body weights; subset them more aggressively.
- Forgetting numerals features. If your tables use
font-variant-numeric: tabular-nums, keep thetnumfeature in the subset or columns stop aligning.
FAQ
Does Google Fonts already subset with unicode-range?
Yes. The Google Fonts CSS API serves per-script subsets with unicode-range, which is one reason it is efficient for multilingual sites. Self-hosting gives you control over caching and connection setup, but you need to replicate the subsetting — see self-hosting Google Fonts.
Is subsetting a licensing problem?
Many commercial font licences restrict modification, which subsetting technically is; many explicitly allow subsetting for web use. Open-licensed fonts (SIL OFL) permit it, though some reserved font names require renaming modified versions. Check the licence before shipping subsets of commercial fonts.
How do I find which characters my site uses?
Tools like glyphhanger crawl pages and output the Unicode ranges used. For dynamic content, add margin — the full Basic Latin and Latin-1 blocks, common punctuation and currency symbols — rather than subsetting to a snapshot of today's text.
Does subsetting change how text looks?
Not for the characters you keep, provided you keep the layout features your typography relies on — kerning, standard ligatures and any numeral styles you use. Visual differences after subsetting almost always come from a dropped feature or a missing glyph rendered in the fallback font, both of which are visible in a side-by-side comparison of key pages before and after the change. Make that comparison part of the pull request that introduces subsetting.
Related
- Font-display swap vs optional for LCP — what happens while the subset downloads.
- Variable fonts vs static font weights — choosing the file structure before subsetting.
- Fixing LCP when the H1 is the LCP element — where font bytes hit LCP.