How to reduce font payloads, improve rendering, and use variable fonts in modern web applications.
Fonts are easy to overlook when optimizing a website. We spend time reducing JavaScript bundles, optimizing images, and tuning server responses—but custom typefaces can quietly add hundreds of kilobytes to the page.
In this guide, we'll walk through four approaches to font optimization:
- Compressing the font files you already use.
- Choosing modern web-font formats such as WOFF2.
- Using built-in font optimization in frameworks such as Next.js 16.
- Switching to variable fonts when the trade-offs make sense.
The goal is not just to ship smaller files. It's to make text visible sooner, avoid unnecessary downloads, and preserve visual stability.
1. Why should we optimize fonts?
A custom web font is a resource the browser has to download before it can display text using that typeface. Depending on how the font is configured, it can cause several performance and usability issues.
Common problems caused by unoptimized fonts
- Larger network payloads. Downloading separate files for Light, Regular, Medium, SemiBold, Bold, and other weights can add significant transfer costs, especially across multiple scripts or language subsets.
- Flash of Invisible Text (FOIT). Some
font-displaychoices can temporarily hide text while waiting for the web font. - Flash of Unstyled Text (FOUT). When fallback text is displayed first and later replaced with the web font, users can notice the change in typography.
- Layout shifts (CLS). Different glyph widths and font metrics can change line breaks and move surrounding elements when the web font arrives.
- Slower initial rendering on limited networks. The effect is especially noticeable when a font used in above-the-fold content is unnecessarily large or delayed.
Fonts can affect Largest Contentful Paint (LCP) when they delay a large text element, and poor metric matching can affect Cumulative Layout Shift (CLS). Not every font optimization improves every metric, so always measure your own application.
Example: 535 KB of font requests
Imagine a page downloading seven different font weights when its initial interface really needs just Regular and Bold.
Illustrative DevTools mockup—not a real production benchmark. The 535 KB figure is an example.
To check your own application:
- Open Chrome DevTools → Network.
- Select the Font filter.
- Enable Disable cache and reload the page.
- Check the transferred size, number of requests, and which files actually loaded.
Remember: the browser doesn't necessarily download every font you declare in CSS. It generally requests the font faces it needs for rendered text, unless preloading changes that behavior.
2. Option one: Compress the font files
Compression is one of the easiest places to start because you can often retain the existing font design, weight choices, and styling.
For example, you can try an online tool such as Font Compressor.
Step by step
- Back up the originals. Keep your source font files unchanged.
- Open a compression tool. Visit fontcompressor.com or another reliable font optimization service.
- Upload eligible fonts. Check that the font's license permits processing and that you're comfortable uploading it to a third party.
- Choose supported optimization options. These might include compression, conversion, or subsetting; available features depend on the tool.
- Download and compare. Compare file sizes, glyph coverage, and the appearance of representative text at every required weight.
- Measure in DevTools again. Compare transfer sizes using the same test conditions.
Important: Removing glyphs can reduce file size, but careless subsetting can break your UI. For multilingual apps, test Persian, Arabic, accented Latin characters, numerals, punctuation, and combining marks. For example, verify both
آandاif your application renders Persian.
You can also use local font tooling when you don't want to upload licensed or private assets to an external service.
3. Option two: Use WOFF2 instead of legacy TTF
The font format also affects transfer size.
| Format | Purpose | General recommendation |
|---|---|---|
| TTF / OTF | Common desktop/source font formats | Keep when needed as source files; not usually the best web delivery format |
| WOFF | Web font container with compression | Useful mainly for older compatibility requirements |
| WOFF2 | Modern, efficiently compressed web font format | Preferred for modern browsers |
WOFF2 generally compresses better than WOFF and uncompressed TTF/OTF. The exact improvement depends on the font and how it was originally encoded.
Here's a basic CSS example:
@font-face { font-family: "Brand"; src: url("/fonts/Brand-Regular.woff2") format("woff2"); font-weight: 400; font-style: normal; font-display: swap; } body { font-family: "Brand", Arial, sans-serif; } WOFF2 conversion is not the same as creating a variable font. Converting to WOFF2 changes how the font is packaged and compressed for the web; a variable font encodes a range of design variations in a font with variation axes.
4. Option three: Use a framework's font optimizer
Modern JavaScript frameworks can automate several parts of font delivery: generating font-face declarations, self-hosting, providing CSS class names or variables, and emitting preload hints.
- Next.js: provides
next/font, includingnext/font/localfor local files. - Nuxt: offers font optimization through its ecosystem, including Nuxt Fonts, with configuration that differs from Next.js.
We'll focus on Next.js 16.x with the App Router.
Step 1: Prepare your fonts
Place the WOFF2 font files alongside your font declaration module:
app/ fonts/ Brand-Light.woff2 Brand-Regular.woff2 Brand-Medium.woff2 Brand-Bold.woff2 fonts.ts layout.tsx globals.css Step 2: Configure next/font/local
Create app/fonts.ts:
import localFont from "next/font/local"; export const brandFont = localFont({ src: [ { path: "./fonts/Brand-Light.woff2", weight: "300", style: "normal" }, { path: "./fonts/Brand-Regular.woff2", weight: "400", style: "normal" }, { path: "./fonts/Brand-Medium.woff2", weight: "500", style: "normal" }, { path: "./fonts/Brand-Bold.woff2", weight: "700", style: "normal" }, ], display: "swap", preload: true, fallback: ["Arial", "sans-serif"], adjustFontFallback: "Arial", variable: "--font-brand", }); The src paths are relative to app/fonts.ts, where you call localFont.
Step 3: Register the font in the root layout
In app/layout.tsx:
import type { ReactNode } from "react"; import { brandFont } from "./fonts"; import "./globals.css"; export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en"> <body className={brandFont.variable}>{children}</body> </html> ); } Step 4: Use your CSS font variable
Because the generated custom property is applied to <body> in the layout above, apply the font-family on body as well:
body { font-family: var(--font-brand), Arial, sans-serif; } If you don't need to reference a CSS variable, you can apply brandFont.className directly to your element instead.
Visual summary of the local font definition. Use the code blocks above as the implementation reference.
Understanding the display option
display controls CSS font-display: what a browser shows while a web font is being loaded and whether the fallback text can be swapped later.
| Value | Behavior | Trade-off |
|---|---|---|
auto | Browser chooses the strategy | Behavior is browser-dependent |
block | Has a temporary invisible-text period, then can show fallback until the font loads | May delay readable text |
swap | Fallback displays immediately and is swapped once the web font loads | Readable text early, but possible visible reflow |
fallback | Very brief block period followed by a limited swap period | May retain fallback if the font loads too late |
optional | Very brief block period; a late font may not replace fallback | Prioritizes stability over guaranteed custom-font appearance |
The following timeline illustrates these strategies:
Font-display timing illustration supplied for this article. The approximately 100 ms and 3 s windows are conceptual/spec-recommended examples, not fixed timings in every browser.
Which value should you choose?
For text-heavy applications, swap is a useful starting point because text stays visible. But if replacing the fallback font produces distracting layout shifts, investigate metric adjustments or test optional. There's no universal winner for every interface.
Understanding fallback, adjustFontFallback, and preload
These Next.js options solve related but different problems:
-
fallbacksets an ordered list of fallback font families. For example,['Arial', 'sans-serif']. -
adjustFontFallbackasks Next.js to generate metric-adjusted fallback styles to reduce layout shift. Withnext/font/local, the supported options include'Arial','Times New Roman', orfalse; the default is'Arial'. -
preloadtells Next.js whether to emit preload hints. The default istrue. Be careful when declaring many static files: preloading fonts that are not needed early can compete with other important resources. -
variableexposes the generated font family through a CSS variable such as--font-brand.
Note: next/font/local does not combine multiple static weight files into a genuine variable font. It automates loading and application; the source files stay separate.
5. Advanced: Variable fonts
A traditional static font file often stores a single instance of a typeface: one weight such as 400 (Regular) or 700 (Bold), for example.
A variable font can represent a range of styles using one font file and one or more axes. These may include:
-
wght: weight, such as 100–900. -
wdth: width, if supported. -
opsz: optical size, if supported. -
slnt: slant, if supported.
This means a single variable WOFF2 file may cover weights such as 300, 400, 500, 600, and 700 without requiring a separate file for each weight.
Why can variable fonts help?
- Fewer font requests when an app actually uses several static weights.
- Continuous weight values for richer typography and animation.
- Simpler asset management with one variable source instead of many individual files.
However, one variable font is not automatically smaller than a couple of well-subsetted static files. If you only use Regular and Bold, the combined size of two static WOFF2 files may be better. Benchmark both strategies.
Can you create a variable font from static font files?
Not automatically. Renaming several static files, merging them into an archive, or converting them into WOFF2 does not make a true variable font.
A proper variable font requires compatible outlines/master designs, interpolation data, and valid variation tables. If the masters differ in contour structure or glyph coverage, the resulting font can render incorrectly—especially for complex scripts.
Best practice: Use an official variable font from the font publisher whenever possible, and verify licensing and supported axes.
Step 1: Obtain a real variable WOFF2 font
In this example, assume your file named BrandVariable.woff2 genuinely supports a weight range from 100 to 900. Don't claim a range the font doesn't actually contain.
Step 2: Configure it with next/font/local
In app/fonts.ts:
import localFont from "next/font/local"; export const brandVariable = localFont({ src: "./fonts/BrandVariable.woff2", weight: "100 900", style: "normal", display: "swap", preload: true, fallback: ["Arial", "sans-serif"], adjustFontFallback: "Arial", variable: "--font-brand", }); Step 3: Apply the variable font
In app/layout.tsx:
import type { ReactNode } from "react"; import { brandVariable } from "./fonts"; import "./globals.css"; export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en"> <body className={brandVariable.variable}>{children}</body> </html> ); } Step 4: Use different weights in CSS
body { font-family: var(--font-brand), Arial, sans-serif; } h1 { font-weight: 700; } p { font-weight: 400; } small { font-weight: 300; } Conceptual comparison of static and variable fonts. The diagram illustrates fewer files, not a guaranteed reduction in total transferred bytes.
A variable font that covers a weight range does not automatically provide an italic font. If your design needs italic, check whether your font actually supports that style or add the correct italic face separately.
6. Verify your optimization
Do not judge an optimization just by looking at the files in your repository. Compare production behavior with the same browser and network settings.
- Build and run the production application (
next build, followed bynext startif appropriate for your deployment). - Open DevTools → Network → Font and test with a cold cache.
- Record request count and transferred bytes, not just the sizes of source files.
- Check which fonts are preloaded but not immediately used.
- Throttle the connection and watch for FOIT, FOUT, and layout movement.
- Review LCP and CLS with Lighthouse or a performance recording.
- Check glyph coverage and line breaks for every language you support.
- Compare both first and repeat visits to understand caching.
It's useful to record a before/after table from your real application:
| Metric | Before | After |
|---|---|---|
| Font requests | Measure | Measure |
| Font bytes transferred | Measure | Measure |
| LCP | Measure | Measure |
| CLS | Measure | Measure |
Don't publish an improvement percentage unless you've measured it.
Conclusion
Typography is part of your application's performance budget. Large font files, unnecessary font weights, unoptimized formats, and poor fallback choices can all make an otherwise fast application feel slower.
Start with straightforward improvements: audit requests, compress carefully, use WOFF2, and load only the weights you actually need. Frameworks such as Next.js 16 make local font loading easier to manage, and font-display lets you choose the right rendering trade-off for your interface.
Then consider variable fonts as an advanced technique when your design uses multiple weights or variation axes—and when real measurements show a benefit.
Measure first. Optimize deliberately. Verify with real text and real users.
References
- Next.js font optimization
- Next.js
next/fontAPI - MDN:
font-display - web.dev: Optimize web fonts
- Font Compressor
Editor's note: the example Network image is a mockup, not an actual DevTools performance result. Replace it with a screenshot from your own application if you want to present a genuine before/after case study.




