How to variable fonts vs static fonts: when each wins
- Step 1Count the distinct weights you actually render — Grep your design system's CSS for `font-weight` and `font-variation-settings`. Count the distinct values from each family. Three or more from one family is the classic point where one variable file beats N static files on total bytes.
- Step 2Account for width and optical-size axes too — If you use more than just weight — `wdth`, `opsz`, `slnt` — variable's advantage grows, because each of those would be a separate static file otherwise. A condensed-plus-regular plus three weights is six static files versus one variable.
- Step 3Weigh caching and render-blocking, not just bytes — Separate static WOFF2s cache independently and load in parallel; a single variable WOFF2 is one cache entry and one (potentially large) download. For a marketing page with one headline weight, a small static subset can beat the full variable file on first paint even though it 'loses' on total bytes.
- Step 4Check your audience's browser floor — Variable fonts need 2018-era browsers. If your analytics show IE11 or pre-2019 Safari/WebKit-view traffic above your tolerance, you need a static fallback regardless of the byte math. [The OpenType features inspector](/font-tools/opentype-features-inspector) confirms a font is variable before you plan around it.
- Step 5If you choose static, decide which instance you need — JAD's [variable font freezer](/font-tools/variable-font-freezer) produces the **default-master** static face — it strips `fvar`/`gvar`/etc. but leaves outlines at the default. If you only need the default weight, that's all you need. For Bold or any non-default instance with real baked outlines, use `fonttools varLib.instancer`.
- Step 6Produce and verify the static fallback — Freeze the default-master static, convert it to WOFF2 with [TTF→WOFF2](/font-tools/ttf-to-woff2), and confirm metrics and kerning survived with [the metrics analyzer](/font-tools/font-metrics-analyzer) and [the kerning-pair auditor](/font-tools/kerning-pair-auditor) before wiring it into your `@font-face`.
Decision matrix: variable vs static
The honest tradeoff across the dimensions that actually decide it. 'Bytes' assumes the same family and glyph coverage; real numbers depend on axis count and subset.
| Dimension | Variable wins when… | Static wins when… |
|---|---|---|
| Total bytes | You ship 3+ weights (or any width/opsz variety) from one family | You ship 1–2 weights and no other axes |
| First paint / LCP | The file is small and not on the critical path | You can inline or preload a tiny critical subset |
| Caching | You serve the same family across many pages and rarely change it | You want per-weight cache invalidation |
| Browser reach | Your audience is ~98%+ modern (2018+) | You must support IE11 / pre-2019 WebKit views |
| Design flexibility | You want continuous weights/grades via font-variation-settings | You only ever use a fixed set of named weights |
| Pipeline simplicity | Your toolchain handles fvar fine | A consumer (PDF lib, old installer) rejects fvar |
Why variable carries overhead
Variable-font size is dominated by gvar, whose cost scales with both glyph count and the number of axes. This is the table that explains the break-even.
| Factor | Effect on variable size | Implication for the freeze decision |
|---|---|---|
| Axis count | Each axis multiplies the per-glyph delta data in gvar | A 5-axis font has far more to strip — freezing the default master can save a lot |
| Glyph count | gvar grows with the number of glyphs that vary | CJK fonts are mostly glyf/loca, so freezing them saves comparatively little |
| Width/optical axes | wdth/opsz reshape many glyphs → large deltas | These are exactly the axes you lose by freezing — keep variable if you use them |
| Named instances | fvar lists them; STAT describes the design space | Both are removed on freeze; the static face is a standalone style |
| Internal compression | WOFF2 brotli compresses gvar well | A variable WOFF2 can be smaller on the wire than an uncompressed static TTF — convert the frozen file to WOFF2 before comparing |
Browser support floor (variable fonts)
Variable-font support is effectively universal on modern browsers; the gap is legacy. If your traffic includes any of the 'no' rows above your tolerance, you need a static fallback.
| Engine | Variable fonts | Note |
|---|---|---|
| Chrome 62+ / Edge 17+ | Yes | Shipped 2018; current Edge is Chromium |
| Firefox 62+ | Yes | Shipped 2018 |
| Safari 11+ / iOS 11+ | Yes | Some early Safari 11 builds had quirks; 12+ is solid |
| IE11 | No | Rejects fvar — needs a static fallback |
| Edge Legacy (EdgeHTML) | No | Pre-Chromium Edge |
| Old embedded WebKit/Chromium views | Sometimes no | Native-app webviews can lag the standalone browser by years |
Cookbook
Worked decisions, not abstract advice. Each shows the weight/axis usage, the call, and what to produce if the call is 'static'.
Marketing site, one headline weight → static
ExampleA landing page uses exactly one weight of one family for the hero. The full variable file is overkill and render-blocking. Ship a small static (ideally subset) and preload it.
Usage: 1 weight (Bold headline), Latin only
Call: STATIC — variable's multi-weight advantage is wasted
Produce: subset to Latin -> freeze default master OR
instance the Bold with fonttools -> TTF -> WOFF2
Preload: <link rel=preload as=font type=font/woff2 crossorigin>Design system, five weights + italic → variable
ExampleA component library uses Light/Regular/Medium/Bold/Black plus italics. That's ten static files versus one variable (plus one variable italic). Variable wins decisively on bytes and on having one source of truth.
Usage: 5 weights x 2 styles = 10 static files Call: VARIABLE (one upright var + one italic var) Fallback for legacy: freeze the 3 most-used weights to static - default-master freeze covers Regular - fonttools instancer for Bold/Light real outlines
App with embedded webview of unknown age → static
ExampleYou ship into a native-app webview you don't control the version of. Variable support is a coin flip. Ship static and skip the risk.
Constraint: webview engine version unknown / possibly <2018 Call: STATIC (no fvar to choke on) Produce: freezer default-master static per needed weight Verify: features-inspector confirms GSUB/GPOS survived
Comparing wire size fairly
ExampleDon't compare a variable WOFF2 against the freezer's uncompressed TTF — that's apples to oranges. Compress both to WOFF2 first.
WRONG: Inter.var.woff2 (90 KB) vs Inter.static.ttf (250 KB)
-> looks like variable won by a mile (it didn't, TTF is raw)
RIGHT: Inter.var.woff2 (90 KB)
vs Inter.static.ttf -> WOFF2 (~110 KB for ONE weight)
-> single static weight is competitive; 5 statics are notOptical-size axis in use → keep variable
ExampleIf your design uses opsz (display vs text optical sizing), freezing throws that away — every size renders from one master. Keep variable, or accept a single optical compromise.
Usage: opsz axis (text 14, display 72) actively used Call: VARIABLE — freezing collapses to ONE optical master If you must freeze: pick the dominant size with fonttools fonttools varLib.instancer Font.ttf opsz=16 -o Font-text.ttf (JAD freezer would keep the default opsz only)
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.
Comparing freezer output bytes against the variable WOFF2
Misleading — compare WOFF2 to WOFF2The freezer emits an uncompressed TTF. Comparing that raw TTF against a brotli-compressed variable WOFF2 makes static look far worse than it is. Convert the frozen TTF to WOFF2 first with TTF→WOFF2, then compare. A single static weight is usually competitive with the whole variable file; it's only at 3+ weights that the totals diverge.
You assumed freezing gives you a Bold static
By design — default master onlyDeciding 'static' and reaching for the freezer gets you the default-master shape, not an arbitrary weight. If your single weight happens to be the font's default, you're done. If you need Bold and the default is Regular, the freezer's output looks like Regular — use fonttools varLib.instancer wght=700 for real Bold outlines.
Width or optical-size axis in active use
Keep variableIf your layout uses wdth (condensed/expanded) or opsz (optical sizing), going static means collapsing those to a single master each. That can visibly degrade display headlines or condensed UI. The byte math may say static, but the design math says keep variable — or freeze one chosen optical/width point and accept the loss.
Italic on an axis vs a separate file
Depends on the familyMost families ship italic as a separate variable file rather than on a slnt/ital axis, so freezing the upright doesn't touch italic. A few advanced families (e.g. some Roboto Flex configurations) put slant on an axis — freezing those collapses to upright. Check the axis list in the OpenType features inspector before assuming.
CJK variable font — static doesn't save much
Expected — glyph-dominatedA CJK variable font's size is dominated by tens of thousands of glyph outlines, not gvar. Freezing strips the variation tables but the glyph bulk remains, so the static file is barely smaller. For CJK, the real lever is subsetting to the characters you use — see the font subsetter — not freezing.
Render-blocking large variable font hurts LCP
Real cost — measure itA 200–400 KB variable file on the critical render path can delay first contentful paint more than a 20 KB static subset would. The total-bytes win doesn't capture this. For above-the-fold critical text, a small static (subset + freeze, or instance) with font-display: swap and preload often beats the full variable file on perceived speed.
Per-weight caching with one variable file
Tradeoff — can't invalidate per weightWith static files, changing only Bold invalidates one cache entry. With a variable file, any change to any axis invalidates the single file for every weight. For frequently-tweaked brand fonts served at scale, separate statics give finer cache control — a point the byte comparison alone hides.
98% support sounds safe but your segment differs
Check your own analyticsGlobal ~98% variable support is irrelevant if your specific audience (an enterprise intranet on IE11, a kiosk on an old webview, a region with older devices) skews legacy. Decide from your own browser analytics, not the global figure. When in doubt, the static fallback in the src list costs only the legacy users a few bytes.
Frequently asked questions
When does variable actually win on size?
When you ship roughly three or more weights from the same family, or use any second axis (width, optical size, slant) at all. Below that — one or two weights, weight-only — separate static files total fewer bytes because you avoid the gvar overhead. Always compare WOFF2 to WOFF2, not the freezer's uncompressed TTF to a variable WOFF2.
Does a variable font support italic?
Usually via a separate italic file (the common convention), not an axis — so a roman variable and an italic variable. Some families put slant on a slnt/ital axis. If yours does and you freeze it, you collapse to upright; check the axis list first with the OpenType features inspector.
What's the browser support for variable fonts in 2026?
About 98% globally — Chrome 62+, Firefox 62+, Safari 11+, Edge 17+. The remaining gap is IE11, Edge Legacy, and old embedded WebKit/Chromium webviews. If your audience includes those above your tolerance, ship a static fallback produced with the freezer.
Why does a variable font carry so much overhead?
The gvar table stores per-glyph outline deltas, and its size scales with both glyph count and axis count. A 1-axis Latin font carries modest gvar; a 5-axis font carries far more. That overhead is exactly what freezing removes — which is why default-master freezing of multi-axis Latin fonts saves the most.
If I go static, does JAD's freezer give me any weight I want?
No — it gives the default-master static. It strips fvar/gvar/HVAR/MVAR/VVAR/cvar/STAT/avar but leaves outlines at the default position, because deltas aren't applied in-browser. For an exact instance (Bold, Light, a custom wght), run fonttools varLib.instancer. See the freeze how-to.
Can I ship both — variable to modern, static to legacy?
Yes, and it's the common mature pattern. Put the variable WOFF2 first in the @font-face src list and the static WOFF2 after; browsers walk the list and pick the first format they support. The fallback pipeline guide shows the build script and the @font-face block.
Is the break-even really exactly three weights?
It's a rule of thumb, not a constant. The true point depends on the family's gvar overhead, your glyph subset, and whether you use other axes. Two heavy-axis weights can already favour variable; four light weights of a tiny subset can still favour static. Measure with WOFF2-vs-WOFF2 for your actual subset.
Does freezing help or hurt page-load speed?
It can help when the variable file is large and on the critical path: a small static subset paints faster. It hurts if you replace one cacheable variable file with many static files for a multi-weight design. The right answer is per-page — judge by what's on the critical render path, not by total bytes alone.
What about Figma and Adobe support?
Figma has read variable fonts (axis values, named instances) since 2022, and Adobe apps support them too. Tooling support is no longer a reason to avoid variable. The remaining reasons are legacy browsers, render-blocking concerns, and pipelines that reject fvar.
Will the static fallback look identical to the variable at its default?
At the default axis position, effectively yes — same outlines, same GSUB/GPOS. The subtle exception is MVAR-driven metrics, which revert to head/OS/2 defaults after freezing. For default-position rendering that's the same thing; only fonts that adjusted metrics off-default will differ.
How do I verify the static fallback before shipping?
After freezing and converting to WOFF2, check the outlines and features with the OpenType features inspector, confirm metrics with the font metrics analyzer, and spot-check kerning with the kerning-pair auditor. Then test the @font-face in a real legacy engine if you can.
Is variable-vs-static a one-time decision?
It's worth re-checking when your weight usage changes or your browser floor moves. Codify the current call in your design system so it isn't re-litigated weekly — the design-system policy guide gives a framework for documenting it.
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.