Subset Inter to under 20KB without breaking your headlines

Inter is the de-facto UI font of the modern web. Vercel uses it. Linear uses it. Most React component libraries default to it. So does this site. The downside: shipped raw, Inter is a 320KB monster covering 2,560 glyphs in 11 scripts.
Most sites need maybe 200 of them.
This is how to subset Inter down to about 20KB while keeping every glyph and OpenType feature you actually use, with examples in fontTools and the Vercel-friendly automation.
What's in Inter you don't need
Inter ships with:
- Latin (Basic + Extended-A + Extended-B + Latin Additional)
- Vietnamese
- Cyrillic + Cyrillic Extended
- Greek
- Polytonic Greek
- Hebrew
- Math symbols
- Number-circled glyphs (①②③ etc)
- Tabular and proportional figures
- Old-style figures
- Multiple ligature sets
- All variable axes (
wght,opsz)
On an English-language website, ~80% of those glyphs never render. Stripping them doesn't change what the user sees. It just makes the file smaller.
The unicode ranges to keep
For virtually every English/Western site, four ranges cover everything:
U+0020 - U+007E Basic Latin (ASCII printable) 95 glyphs
U+00A0 - U+00FF Latin-1 Supplement (©®ñé...) 95 glyphs
U+2010 - U+205F General Punctuation (em/en/'') 96 glyphs
U+20A0 - U+20CF Currency Symbols (€£¥₹) 40 glyphs
That's about 326 unique codepoints. Inter has glyphs for all of them. The browser's text-rendering pipeline never asks for anything else if your content is English with smart typography.
The fontTools recipe
Save this as compress_inter.py:
from io import BytesIO
from fontTools.ttLib import TTFont
from fontTools.subset import Subsetter, Options
from fontTools.varLib.instancer import instantiateVariableFont
# Load Inter variable
font = TTFont('Inter[opsz,wght].ttf')
# Step 1: pin the opsz axis to its default, keep wght+ital range
# This is the BIG win for variable fonts
to_pin = {a.axisTag: a.defaultValue for a in font['fvar'].axes
if a.axisTag not in {'wght', 'ital'}}
if to_pin:
font = instantiateVariableFont(font, to_pin)
# Round-trip serialize so the subsetter sees clean state
buf = BytesIO()
font.save(buf)
font = TTFont(BytesIO(buf.getvalue()))
# Step 2: subset to the unicode ranges we want
options = Options()
options.layout_features = ['*'] # keep kern, liga, calt, smcp, etc.
options.name_IDs = ['*']
options.notdef_outline = True
options.hinting = False # drop fpgm/prep/cvt tables
options.desubroutinize = True # better brotli compression
options.drop_tables = ['DSIG', 'VORG', 'JSTF', 'BASE', 'EBSC', 'EBLC', 'EBDT']
unicodes = (
set(range(0x0020, 0x007F)) | # Basic Latin
set(range(0x00A0, 0x0100)) | # Latin-1 Supplement
set(range(0x2010, 0x2060)) | # General Punctuation
set(range(0x20A0, 0x20D0)) # Currency Symbols
)
subsetter = Subsetter(options=options)
subsetter.populate(unicodes=unicodes)
subsetter.subset(font)
# Step 3: save as WOFF2
font.flavor = 'woff2'
font.save('Inter-Variable.woff2')
Result: 22 KB.
For reference, Google Fonts' default variable Inter (loaded via the CSS API with no axis options) is ~280KB on first request. The subsetted version is 12x smaller.
Verifying it still works
After compressing, render every character your site uses against the new file. Three quick checks:
1. Open the page on a clean browser session, scroll the entire content, watch the network panel, Inter should load once at the new size. 2. Inspect text with smart quotes: 'don't', "hello", 2025,2026, 25%, €100, é, ñ, ü. Each should render correctly with no .notdef boxes. 3. Test with font-feature-settings: "calt" off to confirm contextual alternates still work when you DO want them on.
If any character shows as a fallback font or a .notdef box, the unicode range needs widening. The most common gap is when content uses Latin Extended-A (Polish ł, Czech č, Hungarian ő). Add set(range(0x0100, 0x0180)) to the unicodes set if your audience needs Eastern European scripts.
CSS for the new file
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Variable.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
At 22KB this is small enough that you can preload it confidently:
<link rel="preload" href="/fonts/Inter-Variable.woff2" as="font" type="font/woff2" crossorigin>
First-paint time for Inter on a 4G connection: ~80ms. On WiFi: ~20ms. Functionally instant.
What about Inter italic?
Inter ships italic as a separate variable file: Inter-Italic[opsz,wght].ttf. Same recipe applies, pin opsz, subset, save. Result is also ~22KB.
You can either ship two files (variable upright + variable italic) and reference both in @font-face, OR pick one. Most marketing sites only use upright; only docs sites and editorial layouts need both.
What about the Inter family in 18 individual weights?
If you really need finer control than the variable axis interpolation gives you, the library ships the full set of static cuts (Thin through Black, plus italic variants). Each is ~4-8KB after subsetting. Use the static cuts if:
- You only render 1-2 specific weights and want the smallest possible per-file size
- You're on HTTP/1.1 and parallel downloads of multiple files actually hurt
- Your CSS uses
font-weight: 700to mean specifically the static Bold cut, not interpolated
For everyone else, the variable file at 22KB is simpler.
Doing this without writing code
Drop your Inter TTF onto fontcompressor.com and the same recipe runs server-side. The resulting WOFF2 is what you'd get from the script above, ready to download.
The pre-compressed Inter is also in the library at this exact size, with one click to download or copy the @font-face CSS.
Quick recap
1. Inter has 2,560 glyphs, 11 scripts, multiple variable axes 2. English/Western sites typically need ~330 glyphs 3. Pin every axis except wght (and ital if you ship italic too) 4. Drop hinting + desubroutinize for ~5KB extra savings 5. Drop the unused tables (DSIG, VORG, JSTF, BASE, embedded bitmaps) 6. Keep layout_features = ['*'] so kerning and ligatures still work 7. Set font-display: swap and preload the file at the top of the document
Done right, Inter goes from a 280KB stallion to a 22KB pony that still gallops.
Frequently asked
Will subsetting break my site's accents or smart quotes?
Only if you subset too aggressively. Basic Latin (U+0020 to U+007E) gives you ASCII only, no é, no em-dash, no ©. Add Latin-1 Supplement (U+00A0-00FF) and General Punctuation (U+2010-205F) and you'll cover virtually all Western copy. About 5KB extra.
What's the safe minimum for an English-only site?
Basic Latin + Latin-1 Supplement + General Punctuation + Currency Symbols. That's about 95+95+96+50 = ~336 glyphs. Inter compresses to roughly 22KB at this range with all the OpenType features intact.
Should I keep ligatures and kerning when subsetting?
Yes. They're tiny (a few hundred bytes total) and they're what makes Inter look like Inter. Don't strip GPOS or GSUB tables. The bytes you save aren't worth the rendering quality loss.
Why is my subsetted Inter still 50KB+?
Common cause: you kept the full variable font with all axes. Inter's variable file has wght and opsz axes. Pinning opsz to its default (and keeping just wght) drops the file dramatically. The other usual suspect is keeping hinting tables, drop fpgm/prep/cvt with options.hinting = False.