All writing

Variable fonts vs. subsetting: do you need both?

7 min read

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
StageSizeWeight range intact
Full file, no subset222.2 KBYes
Latin subset50.2 KBYes

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 keptSizeGlyphs
Default features only50.2 KB210
'*'77.9 KB363

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 weightsCombined sizeCompared to the 50.2 KB variable subset
400, 700 (2 weights)31.6 KBThe static files are 37.0% smaller
400, 500, 700, 900 (4 weights)63.3 KBThe 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:

RangeSizeWeight range intact
Latin (U+0000–00FF)50.2 KBYes
Cyrillic (U+0400–04FF)67.8 KBYes

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

  1. Count the weights in your design. For two weights or fewer, subset each static weight and skip the variable font.
  2. For three weights or more, subset the variable font as one file.
  3. Subset the file to the characters or scripts that the page shows. This step gives the largest saving in this article, 77.4% here.
  4. Keep the default layout features on a static file and on a variable file. Do not use the '*' flag.
  5. 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.