How to calculate the bandwidth you save (or waste) choosing a variable font
- Step 1Get the variable font you actually ship — Use the `.ttf`, `.otf`, `.woff`, or `.woff2` master that contains the `fvar` table — the file your `@font-face` `src` points at. The calculator decodes WOFF/WOFF2 to an sfnt internally, so any of those four formats is fine.
- Step 2Upload it — Drop the single file into the tool. There are **no options to set** — the calculator ignores option input entirely and always produces the full 1-9 projection. (The free tier caps uploads at **5 MB** and **1,000 glyphs**; a large family will need Pro.)
- Step 3Read the headline metrics — The result panel shows three numbers: **Variable file** (KB), **Static estimate (per weight)** (KB, = 40% of the variable size), and **Break-even at** N weights.
- Step 4Open the JSON projections array — Each entry is `{static_weight_count, estimated_static_total_bytes, variable_bytes, variable_wins, delta_bytes}`. Find the row that matches the number of weights your CSS actually declares.
- Step 5Compare against your real weight usage — Count the distinct `font-weight` values you load (e.g. 400/600/700 = 3 weights). Look up that row. If `variable_wins` is true, keep the variable file; if false, a static subset is projected lighter.
- Step 6Act on the verdict — If static wins at your weight count, extract single instances with the [Variable Font Freezer](/font-tools/variable-font-freezer), then trim each with the [Font Subsetter](/font-tools/font-subsetter). If variable wins, generate `@font-face` + preload with [Google Fonts CSS Generator](/font-tools/google-fonts-css-generator) and the [Preload Tag Builder](/font-tools/preload-tag-builder).
What the calculator reads and computes
Every field is derived from your uploaded file. The per-weight static figure is a fixed heuristic, not a real static-font measurement.
| Output field | Source | How it's computed |
|---|---|---|
variable_file_bytes | Uploaded file | file.size — the byte size as uploaded, not re-compressed |
glyph_count | opentype.js parse | font.glyphs.length over the decoded sfnt |
axes | fvar table | Number of variation axes; 0 if there is no fvar table |
estimated_static_per_weight_bytes | Heuristic | Math.round(variable_file_bytes * 0.4) |
breakeven_static_count | Heuristic | Math.ceil(variable_file_bytes / per_weight_estimate) → always 3 with the 0.4 multiplier |
projections[] | Loop n=1..9 | {static_weight_count, estimated_static_total_bytes, variable_bytes, variable_wins, delta_bytes} |
Example projection for a 280 KB variable font
Per-weight estimate = round(280 KB x 0.4) = 112 KB. variable_wins flips at the third weight. delta_bytes = static_total - variable_bytes.
| Weights | Static total (est.) | Variable bytes | variable_wins | delta_bytes |
|---|---|---|---|---|
| 1 | 112 KB | 280 KB | false | -168 KB |
| 2 | 224 KB | 280 KB | false | -56 KB |
| 3 | 336 KB | 280 KB | true | +56 KB |
| 4 | 448 KB | 280 KB | true | +168 KB |
| 5 | 560 KB | 280 KB | true | +280 KB |
| 9 | 1008 KB | 280 KB | true | +728 KB |
Cookbook
Real read-outs from the calculator. KB figures are derived strictly from the uploaded file size and the fixed 40% per-weight heuristic — your numbers scale with your file.
Three-weight site: variable just wins
ExampleA marketing site using 400/600/700. At 3 weights the projection crosses break-even and variable comes out ahead.
Upload: Inter-Variable.woff2 (variable_file_bytes 280000) estimated_static_per_weight_bytes: 112000 breakeven_static_count: 3 projections[2] (3 weights): estimated_static_total_bytes: 336000 variable_bytes: 280000 variable_wins: true delta_bytes: 56000 (static would cost 56 KB more)
One-weight blog: static wins by a mile
ExampleA blog that only ever renders body text at weight 400. The single-weight row is decisive.
projections[0] (1 weight): estimated_static_total_bytes: 112000 variable_bytes: 280000 variable_wins: false delta_bytes: -168000 (variable costs 168 KB more for no benefit) Verdict: freeze a 400 instance with /font-tools/variable-font-freezer.
Reading the headline metrics block
ExampleThe three numbers shown in the result panel above the JSON, for the same 280 KB file.
Variable file: 273.4 KB Static estimate (per weight): 109.4 KB Break-even at: 3 weights
A frozen instance uploaded by mistake
ExampleSomeone uploads a single-weight static export instead of the variable master. axes comes back 0 — your tell that the comparison is meaningless.
variable_file_bytes: 78000 glyph_count: 862 axes: 0 <-- no fvar table: this is NOT a variable font breakeven_static_count: 3 The math still runs, but comparing a static file 'against statics' is nonsense.
Wiring the verdict into a perf budget
ExampleThe JSON is plain enough to assert on in a script or paste into a PR.
# pseudo: keep variable only if it wins at our real weight count WEIGHTS=3 jq ".projections[$((WEIGHTS-1))].variable_wins" out.size-compare.json # true -> keep the variable file # false -> freeze + subset static instances instead
Edge cases and what actually happens
Every row below was probed against the live API. Some documented requirements (alphabetical axis order, numerical tuple order) are not actually enforced in practice — useful to know if you've been blaming the wrong thing for a 400.
No file uploaded
Error: Upload a variable font.The handler throws immediately if no file is provided. There is no demo or sample mode — you must supply your own font.
The estimate is a flat 40%, not a real static measurement
Heuristic onlyestimated_static_per_weight_bytes is Math.round(variable_file_bytes * 0.4). The tool does not generate or weigh actual static instances. If your real static weights compress to more or less than 40% of the variable file, the projection will be off in that direction. Treat the numbers as a directional model, not a guarantee.
Break-even is always 3 weights
By constructionBecause the per-weight estimate is exactly 40% of the variable size, Math.ceil(size / (0.4*size)) = Math.ceil(2.5) = 3 for any file. The break-even does not actually depend on your font — it's a fixed property of the 0.4 multiplier. Use the per-weight delta, not the break-even number, for fine decisions.
Uploaded file is already a static font (no fvar)
axes: 0parseFvar returns null when there's no fvar table, so axes is 0. The calculator still emits projections, but comparing a static file against projected statics is not meaningful. Upload the variable master instead.
Variable size is read as-is, not re-compressed
Uses file.sizevariable_bytes is the uploaded file's byte length. Upload an uncompressed TTF/OTF variable and the comparison uses that bulky number; upload the WOFF2 you actually serve for a realistic verdict.
Font over 5 MB on the free tier
Rejected (tier limit)Free tier caps font uploads at 5 MB. Large families or CJK variable fonts exceed this and need Pro (50 MB).
Font over 1,000 glyphs on the free tier
Rejected (glyph limit)Free tier enforces a 1,000-glyph limit. Many variable fonts (full Latin-extended, CJK) exceed it; subset first or upgrade to Pro (65,536-glyph limit).
WOFF / WOFF2 input
Decoded automaticallyWOFF (zlib) and WOFF2 (Brotli) are decompressed to an sfnt before parsing, so the glyph/axis counts are correct regardless of input wrapper. The byte size compared is still the original uploaded file's size.
Corrupt or non-font file
Error: Unsupported font formatFormat is detected by magic bytes. Anything that isn't TTF (0x00010000), OTF (OTTO), WOFF, or WOFF2 throws before any math runs.
Options sent to the tool
IgnoredThe handler signature accepts options but never reads them. There is no weight-count input — you always get the full 1-to-9 projection. Pick your row from the table.
Frequently asked questions
How does the calculator estimate the static weight size?
It uses a fixed heuristic: each static weight is estimated at 40% of your variable file's byte size (Math.round(variable_file_bytes * 0.4)). It does not build or measure real static instances. The number scales with whatever variable file you upload.
Why is the break-even always 3 weights?
Because the per-weight estimate is exactly 40% of the variable size, breakeven = ceil(size / (0.4*size)) = ceil(2.5) = 3 for every font. It's a property of the multiplier, not of your specific file. For finer decisions, read delta_bytes at your real weight count instead.
Does it re-compress my font to WOFF2 to get a fair size?
No. variable_bytes is file.size — the exact size of the file you uploaded. For a realistic verdict, upload the WOFF2 you actually serve, not an uncompressed TTF.
What if I upload a static font by accident?
The axes field comes back 0 (no fvar table). The projections still compute, but the comparison is meaningless. Upload the variable master that has the variation axes.
Can I tell it how many weights I use?
No — there is no input for that. The tool ignores options and always emits a projection for 1 through 9 weights. Look up the row matching your real usage.
How do I count my real weight usage?
Count the distinct font-weight values your CSS loads — e.g. 400, 600, 700 is three weights. That's the projection row to read.
What formats can I upload?
TTF, OTF, WOFF, and WOFF2. WOFF and WOFF2 are decompressed internally before the glyph and axis counts are read.
Is there a file size or glyph limit?
On the free tier: 5 MB per file and 1,000 glyphs. Pro raises these to 50 MB and 65,536 glyphs. Large or CJK variable fonts usually need Pro.
Static wins for my site — now what?
Extract single static instances with the Variable Font Freezer, then trim each one with the Font Subsetter to shave bytes further.
Variable wins for my site — now what?
Keep the variable file and ship it well: build @font-face with the Google Fonts CSS Generator and add a preload with the Preload Tag Builder.
Is my font uploaded anywhere?
Processing is in-browser; the tool reads the file, computes the projection, and returns JSON. Your font is not sent to a server for the calculation.
What's in the downloaded file?
A <stem>.size-compare.json containing variable_file_bytes, glyph_count, axes, estimated_static_per_weight_bytes, breakeven_static_count, and the projections array.
Privacy first
Every JAD Font tool runs entirely in your browser using opentype.js and the wawoff2 WASM Brotli encoder. Your fonts never leave your device — verified by zero outbound network requests during processing.