Type something to search...
Font Subsetting: Shrinking Web Font Files Without Losing Characters

Font Subsetting: Shrinking Web Font Files Without Losing Characters

A modern open-source font can support hundreds of languages. Noto Sans, Inter, and Source Sans all cover Latin, Cyrillic, Greek, and often Vietnamese and other extended Latin alphabets, plus symbols, arrows, and math operators. That's wonderful for multilingual sites, but if your site is in English, every one of those extra glyphs is bytes your visitors download and never see.

Font subsetting is the process of removing characters and features you don't need from a font file. Done well, it can shrink a font to a fraction of its original size with no visible difference. Done carelessly, it produces missing characters: a customer named Zoë whose name renders in a fallback font, or a price in euros where the euro sign looks different from the digits beside it.

This guide covers what's inside a font file, how to choose what to keep, how to subset with pyftsubset, how to split fonts by script with unicode-range, and how to avoid the common ways subsetting goes wrong.

What's Inside a Font File

A font file contains several kinds of data. The ones that matter for subsetting:

  • Glyphs: the outlines for each character, and extra glyphs like alternates, ligatures, and small caps.
  • The character map (cmap): maps Unicode code points to glyphs.
  • OpenType layout tables (GSUB, GPOS): substitution rules for ligatures, alternates, and contextual forms, plus positioning rules like kerning and mark placement.
  • Hinting: instructions that adjust outlines for low-resolution rendering, mostly relevant on Windows.
  • Metadata: names, version strings, license info.

Glyphs are usually the bulk of the file, so removing unused characters gives the biggest savings. Layout features and hinting can also be significant.

Deciding What to Keep

Before running any tool, decide what your site actually needs. The right subset depends on your content, not just your interface language.

By Language

Unicode is organized in blocks. Common building blocks for subsets:

RangeContents
U+0000-007FBasic Latin (ASCII)
U+0080-00FFLatin-1 Supplement: accented letters, ©, °, £
U+0100-017FLatin Extended-A: Central European letters
U+0180-024FLatin Extended-B
U+0370-03FFGreek and Coptic
U+0400-04FFCyrillic
U+2000-206FGeneral Punctuation: dashes, curly quotes, ellipsis
U+20A0-20CFCurrency Symbols, including €

Google Fonts uses a well-known "latin" subset that combines Basic Latin, Latin-1, and a handful of other characters such as typographic quotes, dashes, the euro sign, and the trademark symbol. It's a good default for English and most Western European languages:

U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD

Check the current definition in the Google Fonts CSS for your font if you want to match it exactly, since the list has been adjusted over time.

By Content, Not Just Interface

The interface language isn't the whole story. Think about:

  • Names. User names, author names, and place names often include characters outside ASCII: ł, ő, ş, ñ.
  • User-generated content. Comments, reviews, and profiles can contain anything.
  • Typography. Curly quotes, en and em dashes, the ellipsis, the multiplication sign, and non-breaking spaces are easy to miss if you build a subset from ASCII alone. See Quotes, Apostrophes, and Primes: Getting Punctuation Right for why those characters matter.
  • Currency and units. €, £, ¥, °, µ.

A safe pattern: use an aggressive subset for fonts that only appear in your own controlled content (logos, headings, navigation) and a broader one for body text that might contain user input.

OpenType Features

Subsetting tools keep a default set of layout features and drop the rest unless you ask for them. If you rely on small caps, tabular figures, or stylistic sets, you need to keep those features explicitly. Using OpenType Features in CSS with font-feature-settings lists the common feature tags.

Subsetting with pyftsubset

pyftsubset is part of fonttools, the standard Python library for working with font files. It's what many font services use under the hood.

Install

pip install fonttools brotli

brotli is needed to write WOFF2 output.

A Basic Latin Subset

pyftsubset Inter-Regular.ttf \
  --unicodes="U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD" \
  --layout-features="kern,liga,calt,tnum,case" \
  --flavor=woff2 \
  --output-file=inter-latin-400.woff2

The key options:

OptionWhat it does
--unicodesCode points to keep, as ranges or single values
--textKeep only the characters in a literal string
--text-fileKeep the characters found in a file
--layout-featuresOpenType features to keep; * keeps all of them
--flavorOutput format: woff2 or woff; omit for TTF/OTF
--no-hintingDrop hinting instructions for a smaller file
--desubroutinizeFlatten CFF subroutines, which can help WOFF2 compress CFF fonts

Keep All Features

If you're not sure which features your CSS uses, keeping them all is the safe choice. The file is a little larger, but nothing breaks:

pyftsubset SourceSerif4-Regular.ttf \
  --unicodes="U+0000-00FF,U+2000-206F,U+20AC,U+2122" \
  --layout-features="*" \
  --flavor=woff2 \
  --output-file=source-serif-4-latin-400.woff2

Subset to Exact Text

For a logo or a heading font used only for a fixed string, you can keep just those characters:

pyftsubset Fraunces-Black.ttf \
  --text="TideWave" \
  --layout-features="*" \
  --flavor=woff2 \
  --output-file=fraunces-logo.woff2

This can produce a file of only a few kilobytes. Only do this for text you fully control. If someone edits the heading and adds a letter that isn't in the subset, that letter renders in the fallback font.

Variable Fonts

pyftsubset works on variable fonts too, and keeps the variation data. You can combine it with fonttools varLib.instancer to restrict axis ranges first, for example limiting weight to 300–700, which shrinks the file further. See Variable Fonts in CSS: One File, Endless Styles for an example.

Batch Processing

For a family with several files, a short shell loop saves time:

LATIN="U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+20AC,U+2122,U+2212,U+FEFF,U+FFFD"

for f in src-fonts/*.ttf; do
  name=$(basename "$f" .ttf | tr '[:upper:]' '[:lower:]')
  pyftsubset "$f" \
    --unicodes="$LATIN" \
    --layout-features="*" \
    --flavor=woff2 \
    --output-file="public/fonts/${name}-latin.woff2"
done

Splitting Fonts with unicode-range

Subsetting to one language is fine when you know the language. For multilingual sites, you can instead split a font into several subsets and let the browser download only the ones it needs. The unicode-range descriptor tells the browser which characters each file covers:

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-latin-var.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+2000-206F, U+20AC, U+2122, U+2212, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-latin-ext-var.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
  unicode-range:
    U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
    U+A720-A7FF;
}

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-cyrillic-var.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

All three rules share the same family name. When the page renders, the browser checks which characters actually appear and downloads only the files whose ranges match. An English page fetches the Latin file. A Polish page fetches Latin plus Latin Extended. A Russian page fetches Latin (for digits and punctuation) and Cyrillic.

Two things to know:

  • The ranges must match the file's contents. unicode-range is a promise to the browser. If you claim a file covers a character that it doesn't contain, that character falls back to the next font in the stack.
  • Avoid needless overlap. If two files claim the same characters, the browser may download both for a page that only needed one.

This is exactly how Google Fonts serves fonts, which is why its CSS contains many @font-face blocks per weight.

How Much Can You Save?

It depends entirely on the font. A font with wide language coverage has a lot to remove; a font that only ever supported Latin has very little. Rather than trusting a rule of thumb, measure:

ls -lh Inter-Regular.ttf inter-latin-400.woff2

Compare the original, a WOFF2 conversion of the full font, and your subset. That tells you how much of the saving comes from compression and how much from removing glyphs.

Testing a Subset

Subsetting bugs are quiet: the page still renders, just with a few characters in the wrong font. Check deliberately.

  1. Build a test page with every character you expect: the alphabet in both cases, digits, punctuation including curly quotes and dashes, currency symbols, and a few names with accents.
  2. Inspect the rendered font. In Chrome DevTools, select an element and look at "Rendered Fonts" at the bottom of the Computed tab. It lists each font used and how many glyphs came from each. Any fallback means a missing character.
  3. Test OpenType features you use, such as font-variant-numeric: tabular-nums, to confirm they still work.
  4. Test real content, especially pages with user names and comments.

You can also print how many characters a subset contains, and which ones, with fonttools:

python3 -c "from fontTools.ttLib import TTFont; cmap = TTFont('inter-latin-400.woff2').getBestCmap(); print(len(cmap)); print(''.join(chr(c) for c in sorted(cmap)))"

Warning

Check the license before subsetting a commercial font. Some web licenses treat subsetting as modification and don't allow it, or only allow it through the foundry's own tools. Open-source licenses like the OFL generally allow it. This isn't legal advice; when in doubt, ask the foundry.

Common Mistakes

  • Subsetting to ASCII only. You lose curly quotes, dashes, the ellipsis, and accented letters that appear in names.
  • Dropping layout features you use. Ligatures, tabular figures, or small caps silently stop working.
  • Exact-text subsets for editable content. One new character in a CMS heading and the font falls back mid-word.
  • unicode-range that doesn't match the file. The browser trusts the declared range, and characters outside the real file fall back.
  • Forgetting the italic and bold files. Every style needs the same subset, or the bold version of a word might be missing a character the regular version has.
  • Not re-subsetting after a font update. If you update the source font, regenerate the subsets from the new version.

Conclusion

Font subsetting removes the glyphs, features, and hinting your site doesn't use, and it's one of the most effective ways to shrink web fonts. Decide what to keep based on your real content, not just your interface language. Use pyftsubset to produce WOFF2 subsets, keep the OpenType features your CSS relies on, and split multilingual fonts with unicode-range so the browser downloads only what each page needs.

Then test. Build a page with every character you expect, check the rendered fonts in DevTools, and look for fallbacks. A subset that's a little larger but never drops a character is better than a tiny file that breaks someone's name.

Share :

Related Posts

A Short History of Typography: From Gutenberg to Variable Fonts

A Short History of Typography: From Gutenberg to Variable Fonts

When you write font-weight: 600 or font-style: italic, you're using vocabulary that is hundreds of years old. "Italic" comes from Renaissance Ita

Continue Reading
Accessible Typography: Font Size, Contrast, and Spacing Guidelines

Accessible Typography: Font Size, Contrast, and Spacing Guidelines

Most accessibility work on a website involves text. Navigation labels, form instructions, error messages, article bodies, button captions: if people

Continue Reading
Bauhaus Typography and Its Influence on Modern Design

Bauhaus Typography and Its Influence on Modern Design

Bold sans-serif type, thick black bars, red accents, text running at an angle, and lowercase-only headlines. If a poster or website uses that combina

Continue Reading