I Was Shipping Five Font Files. One Variable Font Replaced Them All.
Regular, Medium, Semibold, Bold, Bold Italic — five @font-face blocks, five network requests, just to render a design system's type scale. A single variable font file replaces all of them, and it's been supported everywhere since 2018.

Open the network tab on a site with a half-decent type system and count the font requests. Four is normal. Six isn't rare. Every one of them is a separate .woff2 file for a separate weight — Regular, Medium, Semibold, Bold — sitting in the critical path before the browser can paint text in anything but a fallback font.
Our design system had five: four weights plus a bold italic for pull-quotes. Nobody had decided that on purpose. It accumulated — a designer added Semibold for one component, Bold Italic followed for another, and eighteen months later the font folder looked like a weight had been added every time someone liked how it looked in Figma.
When this shows up in a performance audit, the fix is almost always the same: cut weights. Drop from five down to three. Merge Semibold into Medium. Ship Bold Italic as a font-style: oblique of regular Bold instead of a dedicated file.
This genuinely helps — three requests beat five. It also doesn't touch the actual shape of the problem: every weight you keep is still its own file, its own request, its own round trip. Cut too aggressively and you lose the weight your design actually needs; cut too little and you're back where you started with a slightly smaller number.
Before you keep reading — guess how many of those weights the browser actually downloads on a page that only uses Regular body text and one Bold heading. The honest (and slightly surprising) answer is: two. Browsers don't fetch an @font-face file the moment it's declared — the CSS Fonts spec requires user agents to download only the faces a page's rendered text actually calls for, so Medium, Semibold, and Bold Italic never leave the server for this particular page. That's the one thing "cut weights" already gets right, mostly by accident. What it doesn't fix: those two downloads are still two separate round trips, and the moment any other page in the same design system reaches for a third weight — a Semibold stat callout, a Bold Italic pull-quote — that page pays for a third file, one request at a time, because nothing about a static setup can serve more than the single weight baked into each one.
Here's what the "cut weights" fix never addresses: a static font file is a fixed snapshot. One file draws letterforms at exactly 700 and nothing else. Want 650 for a hover state that should feel "almost bold but not quite"? You can't — 650 doesn't exist unless you shipped a sixth file for it. The weight scale looks continuous in your design tool and is actually a handful of hardcoded stops in production.
A variable font isn't a set of snapshots. It's one file that encodes the whole range as interpolation data — the letterforms at 100 and the letterforms at 900, with the browser computing everything in between on the fly. Ask for font-weight: 538 and you get a real, rendered 538, not a browser fallback or a rounded guess.
That's the whole type system. One @font-face block. One network request. Every weight from thin to black, plus every stop in between, available from the same file:
Five requests become one. The five-weight design system from the top of this post could have shipped as a single file from day one — nobody needed to decide anything about which weights to keep, because the weight is no longer a file, it's a number.
Variable fonts expose more than weight. The OpenType spec registers five standard axes: weight (wght), width (wdth), slant (slnt), italic (ital), and optical size (opsz) — plus whatever custom axes an individual typeface adds on top (a "grade" axis some foundries ship for tuning contrast in dark mode is a common one). The low-level way to reach any axis by its tag is font-variation-settings:
It works, but it's the wrong default for four of those five registered axes — wght, wdth, ital, and slnt already have real CSS properties (font-weight, font-stretch, font-style: italic / oblique <angle>) that do the same job and play better with the rest of CSS: they animate and transition the way you'd expect, they interact correctly with font-weight: bolder/lighter inheritance, and they don't silently override each other the way two font-variation-settings declarations on the same element do. opsz is the odd one out — CSS only gives you an on/off switch for it (font-optical-sizing: auto, which is also the default), not a property that sets a specific number — so pinning an exact optical-size value, or reaching a font's own custom axes, is what font-variation-settings is actually for.
Runs right in your browser — poke at it and watch the concept react live.
A variable font file is not automatically smaller than a single static one. It has to encode interpolation data for the entire range it covers, so a variable file is often larger than any one static weight on its own — sometimes close to the size of two or three static files combined, especially once a large character set (Cyrillic, Greek, a wide set of Latin diacritics) is involved. The win isn't "smaller file." It's "one request instead of several," and that trade only pays off once you're actually using three or more weights. A page that only ever renders Regular text has nothing to gain from swapping to a variable font — a single static .woff2 is still the smallest, fastest thing you can ship.
The other honest caveat: this isn't new. Safari and Chrome shipped variable font support in 2017; Firefox and Edge followed in 2018. There's no legacy-browser excuse here — the fix has been sitting in every evergreen browser for the better part of a decade, available the moment you swap the font file and the @font-face block that points at it.
Our five-weight type system is now one file and one @font-face block, and the button hover state finally gets the in-between weight it always should have had instead of jumping straight from 400 to 700. Nothing about the design changed. The number of HTTP requests it took to render it did.
Open your own network tab and count the font requests on something you shipped this year. If it's more than two, how many of those files are actually different weights of the exact same typeface — and what would happen to your render time if they were one?
Think it clicked? Take the 7-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
- Your 'Save' Button Makes Copies. The File System Access API Doesn't.
- Your browser renders everything, even what you can't see —
content-visibility: autofixes that - You Added
will-changeto Fix the Jank. You Made It Worse.
🚀 Want more like this? Every guide, playground, and quiz lives on bestpractic.org — open it and sign up free so the next one finds you.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
Keep reading
One post a day, in your inbox
Each one with a runnable playground and a quiz. No pitch, no digest, unsubscribe in one click.
0 comments
Sign in to join the discussion, like comments, and save articles for later.