A site self-hosts four static weights of one family (400, 500, 600, 700). When does replacing them with a single variable font actually reduce bytes, and when does it make things worse?
answer
- one outline set plus deltas, not four
- cheap per extra weight, expensive to start
- crossover around three or four weights
- cannot fetch just one weight out of it
- pin or narrow the axes you never vary
basics
~20 sA variable font stores one set of outlines plus delta data describing how they change along an axis, so it beats three or four separate static weight files but loses to one or two. Using few weights, or shipping the full axis range when you need a slice of it, makes it heavier.
solid answer
~50 sA static font file carries a complete set of outlines per weight, so four weights means four full outline sets. A variable font carries one master set plus deltas for the weight axis, which is much cheaper per additional weight — so the crossover is usually somewhere around three or four weights, depending on family and axis count. Above that, one variable file is both fewer bytes and one request instead of four, and it removes the case where a bold appears late and triggers a second font swap. Below it, the axis data is dead weight: a page that only ever renders 400 and 700 may well be smaller with two static instances. Two things move the crossover in your favour: subset the variable font like any other, and narrow or pin unused axes — `fonttools varLib.instancer` produces a file limited to, say, a 400-700 weight range instead of the full 100-900. Measure the actual files rather than trusting the general rule.
code
css · 10 lines@font-face {
font-family: "Fam";
src: url(/fonts/fam-var.woff2) format("woff2-variations");
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
h1 { font-family: "Fam", sans-serif; font-weight: 700; }
p { font-family: "Fam", sans-serif; font-weight: 400; }go deeper
Know that a variable font is one file that can render many weights, where static fonts need one file per weight, and that CSS still asks for a weight with font-weight.
Explain the storage model — one master outline set plus per-axis deltas — and why that makes the crossover land around three or four weights. Be ready to say what the two-value font-weight descriptor is for.
Show that you would measure rather than assume: build both candidates from the real family, compare transferred bytes for a typical page, and account for the extra swap that a late-arriving static weight causes.
Frame it as a design-system decision. How many weights the system is allowed to define drives the answer, and instancing unused axes out of the shipped file is a build-pipeline responsibility, not a per-page choice.
## How a variable font stores weights A static font file describes each glyph once, as an outline. Four weights means four files, each with its own full set of outlines, its own metrics tables and its own layout tables. Most of that is duplicated information — a Medium and a SemiBold of the same design share their structure and differ in stroke thickness and spacing. A variable font stores a single master set of outlines plus **delta data**: for each glyph point, how it moves as you travel along a **design axis**. Registered axes include `wght` (weight), `wdth` (width), `slnt`/`ital` (slant/italic) and `opsz` (optical size); a family may also expose custom axes. The browser interpolates an instance at whatever value CSS asks for, so `font-weight: 520` is a legal, renderable value rather than a rounding to the nearest shipped weight. In CSS, one face declares the range: ```css @font-face { font-family: "Fam"; src: url(/fonts/fam-var.woff2) format("woff2-variations"); font-weight: 100 900; } h1 { font-weight: 700; } ``` The two-value `font-weight` descriptor is what tells the browser this face covers the whole range — get it wrong and the browser will synthesise a fake bold instead of using the axis. ## Where the crossover sits The delta data for one axis costs meaningfully less than a second full outline set, so the byte curve for a variable font is a high starting point with a shallow slope, versus a low starting point with a steep slope for statics. One weight: statics win clearly. Two: statics usually still win. Three or four: it depends on the family, and this is where teams should actually measure. Five or more, or any use of intermediate weights: the variable font wins comfortably. The honest answer in an interview is that the crossover is family-specific and you check it with the real files, because it depends on glyph count, axis count and how much of the file is layout tables that neither approach duplicates well. ## Non-byte reasons the variable file can still win - **One request, one swap.** Four static faces are four fetches, and any weight that only appears further down the page arrives later and causes its own swap. One file means the whole family is present at once. - **Design range.** Intermediate weights, and animating weight, are only possible with an axis. If the design calls for that, statics cannot do it at any size. - **Cache behaviour.** One file invalidates as a unit. That is a win when the family changes rarely and a loss when you would otherwise be shipping only the Latin regular to most visitors. ## Non-byte reasons it can lose - **All-or-nothing download.** You cannot fetch "just the regular" out of a variable file. If most pages only need one weight, statics let the browser take exactly that. - **Unused axes are pure cost.** A family shipping `wght`, `wdth` and `opsz` charges you for all three even if you only vary weight. ## Making the variable file smaller Two levers, and they compose: 1. **Subset it** exactly like a static font — a variable font is still a font file with a glyph inventory, and `pyftsubset` handles it. 2. **Instance it.** `fonttools varLib.instancer` produces a new file with axes pinned to a single value or limited to a sub-range: ```bash fonttools varLib.instancer fam-var.ttf wght=400:700 --output fam-400-700.ttf ``` Pinning an axis you never vary removes its deltas entirely; narrowing one you do vary removes the deltas outside the range you ship. On a family with several axes this is often a larger saving than anything else you can do to the file. ## How to decide Count the weights and styles the design system actually renders — not the ones the family offers. Build both candidates: the subsetted statics for exactly those weights, and the subsetted, instanced variable file. Compare the transferred bytes for a typical page, not the total on disk, since with statics a given page may not fetch all of them. Then factor in whether intermediate weights or weight animation are part of the design, because that decides it regardless of the byte count. Also remember italics: unless the family exposes an `ital` or `slnt` axis, the italic is a second file either way.
- Why can a variable font still lose to statics on a page that renders exactly one weight?Because the file is all-or-nothing. The browser must download the whole thing, including delta data for every weight and every axis the family exposes, to render the single instance you asked for. A subsetted static of just that weight has none of that. Instancing the variable file down to a pinned weight removes the difference, at which point it is effectively a static.
- Does subsetting work on a variable font the same way it does on a static one?Yes — it is still a font file with a glyph inventory, and pyftsubset handles it. Subsetting and instancing are separate levers though: subsetting drops glyphs, instancing drops axis deltas. On a multi-axis family, pinning the axes you never vary is frequently the bigger win of the two.
- What happens if the @font-face rule for a variable font declares a single font-weight value?The browser treats the face as covering only that weight. Asking for a bolder weight then falls outside the declared range, and you get synthetic emboldening — the browser thickening the outlines itself — instead of the real design along the axis. Declare the range with the two-value form, for example font-weight: 100 900.
saying these in an interview costs you the question
- Assumes a variable font is always smaller than statics
- Thinks each axis instance is stored as a separate outline set
- Ships the full axis range when only 400-700 is used
- Declares a single font-weight on a variable face
- Forgets italic is usually a second file regardless