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:

  1. Compressing the font files you already use.
  2. Choosing modern web-font formats such as WOFF2.
  3. Using built-in font optimization in frameworks such as Next.js 16.
  4. 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-display choices 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 Chrome DevTools Network view showing seven font files adding up to approximately 535 KB.

Illustrative DevTools mockup—not a real production benchmark. The 535 KB figure is an example.

To check your own application:

  1. Open Chrome DevTools → Network.
  2. Select the Font filter.
  3. Enable Disable cache and reload the page.
  4. 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

  1. Back up the originals. Keep your source font files unchanged.
  2. Open a compression tool. Visit fontcompressor.com or another reliable font optimization service.
  3. Upload eligible fonts. Check that the font's license permits processing and that you're comfortable uploading it to a third party.
  4. Choose supported optimization options. These might include compression, conversion, or subsetting; available features depend on the tool.
  5. Download and compare. Compare file sizes, glyph coverage, and the appearance of representative text at every required weight.
  6. 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, including next/font/local for 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.

Illustrated Next.js 16 localFont configuration, including swap and fallback settings.

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 diagram comparing block, swap, fallback, and optional with invisible, fallback, and webfont states.

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:

  • fallback sets an ordered list of fallback font families. For example, ['Arial', 'sans-serif'].
  • adjustFontFallback asks Next.js to generate metric-adjusted fallback styles to reduce layout shift. With next/font/local, the supported options include 'Arial', 'Times New Roman', or false; the default is 'Arial'.
  • preload tells Next.js whether to emit preload hints. The default is true. Be careful when declaring many static files: preloading fonts that are not needed early can compete with other important resources.
  • variable exposes 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; } 

Diagram comparing multiple static font weights with one variable WOFF2 file and a Next.js localFont definition.

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.

  1. Build and run the production application (next build, followed by next start if appropriate for your deployment).
  2. Open DevTools → Network → Font and test with a cold cache.
  3. Record request count and transferred bytes, not just the sizes of source files.
  4. Check which fonts are preloaded but not immediately used.
  5. Throttle the connection and watch for FOIT, FOUT, and layout movement.
  6. Review LCP and CLS with Lighthouse or a performance recording.
  7. Check glyph coverage and line breaks for every language you support.
  8. 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

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.