Variable fonts vs. subsetting: do you need both?
A variable font is one file with more than one weight inside it. A subset is a font with unused glyphs removed. These are two different things. This article measures both on one real font.
The test font is Roboto, weight 100 to 900, width 75 to 100. The article uses
fonttools and pyftsubset on this one font. You can run the same commands on your
font and get your own numbers.
A variable font is not automatically small
Convert the variable font to WOFF2 with no subset step. The result is 222.2 KB. This file has every weight from 100 to 900 and both widths. Compare this to one static Roboto 400 in WOFF2, at 217.2 KB. The two files are close in size. The variable font does the work of nine static weights. The static file does the work of one.
A variable font saves file count, not file size, before you subset it. Nine HTTP requests become one request. The byte count is a separate question. A subset answers that question.
Subset a variable font and keep the weight range
A subset does not force a variable font back to one weight. pyftsubset can remove
unused characters and keep the fvar and gvar tables:
pyftsubset RobotoVF.ttf \
--unicodes=U+0000-00FF \
--layout-features=kern,liga,clig,calt,ccmp,locl,mark,mkmk,rlig \
--flavor=woff2 \
--output-file=roboto-latin-variable.woff2
| Stage | Size | Weight range intact |
|---|---|---|
| Full file, no subset | 222.2 KB | Yes |
| Latin subset | 50.2 KB | Yes |
The Latin subset removed 77.4% of the file. The output still has every weight from 100 to 900. A subset step does not remove the weight range. This article uses the same 194-character Latin set as the first article on static Roboto, so the two sets of numbers compare directly.
The all-features mistake costs the same on a variable font
The first article named one mistake: the flag --layout-features='*'. This flag
costs the same tax on a variable font:
| Layout features kept | Size | Glyphs |
|---|---|---|
| Default features only | 50.2 KB | 210 |
'*' | 77.9 KB | 363 |
The '*' flag makes the file 55% larger for the same 194 characters. This flag
keeps every alternate glyph across every weight in the file, not only the default
weight. A variable font has more of these extra glyphs than a static font. Keep the
default feature list on a variable font, the same as on a static font.
Static weights win below three weights
Most comparisons skip this number. This article built four static weights from the
same variable font with fonttools varLib.instancer. Each static weight then went
through the same Latin subset step:
| Static weights | Combined size | Compared to the 50.2 KB variable subset |
|---|---|---|
| 400, 700 (2 weights) | 31.6 KB | The static files are 37.0% smaller |
| 400, 500, 700, 900 (4 weights) | 63.3 KB | The variable file is 20.6% smaller |
Two static weights beat the variable subset. Four static weights lose to it. The crossover point for this font is three weights. Below three weights, the static files win: they do not carry data for weights you do not use. At three weights or more, the variable file wins: it stops adding a full glyph set for each new weight.
Count the weights in your design before you pick a format. A design with two weights does not need a variable font. A design with five weights does not need five files.
A script subset keeps the weight range too
A Cyrillic subset of the same file, instead of a Latin subset, keeps its weight range too:
| Range | Size | Weight range intact |
|---|---|---|
| Latin (U+0000–00FF) | 50.2 KB | Yes |
| Cyrillic (U+0400–04FF) | 67.8 KB | Yes |
Serve these files the same way as static per-script subsets. Use one @font-face
rule for each range, with the same font-family name and a different
unicode-range value. Point each rule at a different subset file. The browser
downloads only the range that the page uses. A weight range inside the file does not
change how unicode-range selection works.
The steps, in order
- Count the weights in your design. For two weights or fewer, subset each static weight and skip the variable font.
- For three weights or more, subset the variable font as one file.
- Subset the file to the characters or scripts that the page shows. This step gives the largest saving in this article, 77.4% here.
- Keep the default layout features on a static file and on a variable file. Do not
use the
'*'flag. - After a subset step on a variable font, check that the weight range stayed intact:
python -c "
from fontTools.ttLib import TTFont
f = TTFont('roboto-latin-variable.woff2')
print('fvar' in f, [a.axisTag for a in f['fvar'].axes] if 'fvar' in f else 'not variable')"
CAUTIONA missing fvar table after a subset step means a step in your pipeline
removed the weight range. The file still loads. The file still shows text. The file
no longer answers to font-variation-settings or to a variable font-weight value.
Every number in this article came from one real variable Roboto file (weight
100–900, width 75–100), fonttools 4.61, and varLib.instancer for the static
weights. A different font can move the crossover point. Measure your own font before
you pick a format.