Web font performance in 2026: subset, self-host, swap
Subsetting cuts a font file by 80%, WOFF2 compresses 30% tighter than WOFF, and size-adjust removes swap layout shift. The self-host playbook we run.
By the end of this playbook your site serves its typeface as a subsetted WOFF2 file, first-party, with a fallback tuned so that the moment the real font arrives, nothing on the page moves. That is the gap between a Largest Contentful Paint under 1.5 seconds and one that drifts past 3 while the browser blocks on a font request.
Fonts are one of the most overlooked performance costs on the web. A single unoptimised family can add 500KB to a page and delay readable text by seconds (web.dev). After Google's 2026 core updates, Core Web Vitals stopped acting as a soft tiebreaker, so a slow or unstable font is now a ranking and conversion cost, not an aesthetic footnote. Here is the exact sequence we run on every Next.js SaaS we ship.
What you need before starting
- The font files in a source format (TTF or OTF), or a license that lets you self-host them. Check the license first: not every foundry permits self-hosting.
- A build step or a CDN you control for serving static assets.
- A way to measure: Chrome DevTools, Lighthouse, and one field-data tool (we use DebugBear or the Chrome UX Report).
Step 1: Audit what you actually load
Open the Network panel, filter by font, and reload. Most sites request four to eight font files and use two. Every weight and style you load is a separate download on the critical path. Write down the families, weights, and styles that appear in real rendered text, not the ones defined in CSS in case someone needs them. This short list is the only thing you will ship.
Step 2: Serve WOFF2, and nothing else
WOFF2 uses Brotli compression and runs about 30% smaller than the older WOFF format, with support in every browser released in the last several years (web.dev). In 2026 there is no case for shipping TTF, EOT, or WOFF beside it. One format, one request per face.
Step 3: Decide variable or static, then subset
A variable font holds every weight and style in one file using interpolation data, so it weighs more than a single static weight (roughly 1.5 to 2.5 times) but far less than the collection it replaces. If your design uses three or more weights, a single variable WOFF2 of 100 to 200KB usually beats 6 to 12 static files totalling 400 to 800KB, cutting the payload by half or more. If you use exactly one weight, a subset static file wins. As a rough marker, a custom variable font subset to Latin often lands around 50 to 60KB as WOFF2, small enough to preload without guilt.
Then subset. Strip every glyph you do not render. A Latin-only site does not need Cyrillic, Greek, or the full symbol range. Subsetting with a tool such as glyphhanger, subfont, or fonttools shrinks a file by 80% or more. Subset per language when you serve several, and let the browser choose the right file with unicode-range.
Step 4: Self-host, first-party, with a CDN in front
Move the files onto infrastructure you control. Self-hosting removes a third-party connection (the DNS lookup, TLS handshake, and round trip to fonts.gstatic.com that a Google Fonts link costs) and makes every font request first-party. It also closes a GDPR question, since a third-party font CDN sees your visitors' IP addresses, a point German courts have already ruled on. Self-hosting does not mean giving up a CDN: put your own CDN in front of the origin so files cache close to users. You own the origin, the CDN is the accelerator.
Step 5: Preload the one font above the fold
Add a single <link rel="preload" as="font" type="font/woff2" crossorigin> for the face your first visible text uses. This tells the browser to fetch it early instead of discovering it deep inside the CSS. Preload one file, not five: overloading the preload queue pushes more important resources back. The crossorigin attribute is required even for same-origin fonts, and leaving it off is the most common reason a preload silently does nothing.
Step 6: Set font-display by role
font-display controls what the browser shows while a font loads. Apply it per role rather than globally.
- swap for the main body weight, so text is visible immediately in a fallback and swaps when the real font is ready. Text is never invisible.
- optional for a preloaded face, where the browser uses the font only if it arrives almost instantly and otherwise keeps the fallback for the whole session. This removes swap shift on that face.
- fallback for secondary weights: a short block period, then a permanent fallback on slow connections.
Step 7: Kill the swap shift with metric overrides
The swap from fallback to web font causes layout shift when the two fonts have different metrics. Fix it in CSS. Declare a fallback @font-face that points at a local system font and correct its metrics to match your web font with size-adjust, ascent-override, descent-override, and line-gap-override. These descriptors are part of CSS Fonts Level 4 and have been baseline since 2023 (MDN). Tuned correctly, the fallback occupies the same space and the swap moves nothing. On Next.js, next/font generates these adjusted fallback metrics automatically and self-hosts the files at build time (Next.js docs), which handles Steps 4, 5, and most of 7 for you.
Step 8: Cache like you mean it
Font files rarely change. Serve them with a long Cache-Control: max-age (a year is standard) and a content hash in the filename so a new version busts the cache cleanly. A returning visitor should never download the same face twice.
Verifying it works
Reload with a cold cache and watch three numbers. In Lighthouse, confirm no "Ensure text remains visible during webfont load" warning. In the Network panel, confirm one WOFF2 per face and a preload that fires early. In field data (Chrome UX Report or DebugBear), confirm Cumulative Layout Shift stays under 0.1 and that no shift is attributed to text. If CLS still moves on font load, your fallback metrics are off, so return to Step 7.
Common failures and fixes
- Preload does nothing. The
crossoriginattribute is missing, so the browser treats the preload and the real request as two separate fetches and downloads the font twice. - Text flashes invisible (FOIT). font-display sits at its default of
auto. Setswapon the body face. - The variable font is heavier, not lighter. You ship one weight. Subset a static file instead.
- A third party sneaks back in. A stray Google Fonts
@importin a stylesheet re-adds the connection you removed. Search for fonts.googleapis.com before you ship. - CLS on load. Fallback metrics are untuned. Add size-adjust and the override descriptors from Step 7.
Going further
Fonts are one input to Core Web Vitals; see how we ship Core Web Vitals in green on Next.js for the rest. The metric overrides in Step 7 lean on the CSS features covered in modern CSS in 2026. And serving type first-party follows the same logic as removing Tailwind from production: fewer third parties, more control over what ships.
Sources
Frequently asked questions
- Is a Google Fonts CDN link still fine in 2026?
- For a quick prototype, yes. For production, self-hosting wins on two fronts. Performance: a Google Fonts link costs an extra DNS lookup, TLS handshake, and round trip to a third-party origin before the font even downloads, and browser cache partitioning since 2020 means the file is no longer shared across sites the way it once was. Privacy: a third-party font CDN sees your visitors' IP addresses, which German courts have already treated as a GDPR issue. Self-hosting a subset WOFF2 removes both problems, and you can still put your own CDN in front of the files.
- Does next/font handle all of this automatically?
- Most of it. next/font self-hosts the files at build time, so there is no third-party request, and it generates a fallback @font-face with adjusted size-adjust and metric overrides to cut layout shift. That covers Step 4, the metric side of Step 7, and it wires up preload for you. What it does not decide for you is Step 1 (which weights you actually need) and Step 3 (variable versus static, and how aggressively to subset a custom font). You still audit and trim; next/font handles the plumbing once you have.
- How much does font optimization actually move Core Web Vitals?
- Two metrics feel it directly. Largest Contentful Paint improves when your heading or hero text renders in a preloaded, first-party WOFF2 instead of waiting on a third-party download, often a few hundred milliseconds on a real connection. Cumulative Layout Shift is the bigger win: an untuned font swap is a common source of visible shift, and correcting fallback metrics can take that contribution to near zero. Neither is a headline number on its own, but they are cheap to fix and both feed a threshold that changes rankings after the 2026 updates.
- Variable font or static: which is faster?
- It depends on how many weights you use, and the honest answer is not always the variable one. A variable file carries interpolation data, so it is 1.5 to 2.5 times heavier than a single static weight. If your design genuinely uses three or more weights, the variable file wins because it replaces 6 to 12 static files with one download. If you use one or two weights, subset static files are smaller and load faster. Count the weights in your rendered text first, then choose.
Studio
Start a project.
One partner for the digital product you need to build. Faster delivery, modern tech, lower costs. One team, one invoice.