Skip to content

Font Optimization — WOFF2, Subsetting, Preload Guide

DodaTech Updated 2026-06-23 7 min read

In this tutorial, you will learn how to optimize web fonts to eliminate invisible text, reduce layout shifts, and decrease font file sizes by up to 90 percent. Web fonts often cause performance problems including Flash of Invisible Text (FOIT) and Flash of Unstyled Text (FOUT). DodaTech reduced font-related page weight by 75 percent through systematic optimization.

What You Will Learn

  • How to convert fonts to WOFF2 format for optimal compression
  • How to subset fonts to include only needed characters
  • How to use font-display descriptors to control text rendering behavior
  • How to preload critical fonts for early discovery

Why It Matters

Unoptimized fonts can add 200-500KB of page weight, delay text rendering by several seconds, and cause significant layout shifts. A study by DodaTech found that pages with font-display: swap retained 7 percent more users than those using the default font-display: block.

Real-World Use Case

The DodaTech documentation site used Google Fonts with three weights of Inter and two weights of JetBrains Mono, totaling 480KB. After subsetting to Latin characters only and converting to WOFF2, total font weight dropped to 64KB and text became visible within 200 milliseconds.

Prerequisites

You should understand CSS @font-face rules and how Core Web Vitals affect page rendering. Familiarity with command-line tools is helpful.

Step-by-Step Tutorial

Step 1: Use WOFF2 Format

WOFF2 provides 30 to 50 percent better compression than WOFF and is supported by 97 percent of browsers.

# Convert TTF to WOFF2 using Google woff2 tool
sudo apt-get install woff2
woff2_compress Input.ttf -o Output.woff2

# Or use Python (fonttools)
pip install fonttools
pyftsubset Input.ttf --output-file=Output.woff2 --flavor=woff2

Expected output: A WOFF2 file that is significantly smaller than the original TTF. Compare sizes: TTF 160KB, WOFF 90KB, WOFF2 45KB.

Step 2: Subset Fonts to Needed Characters

Subsetting removes unused characters from the font file. For an English site, you only need Latin characters, numbers, and punctuation.

# Subset a font to basic Latin characters only
pyftsubset Inter-Variable.ttf \
  --unicodes="U+0020-007F,U+00A0-00FF,U+0100-02AF" \
  --output-file=Inter-subset.woff2 \
  --flavor=woff2

Expected output: The font drops from 160KB to approximately 18KB for basic Latin subset.

For Google Fonts, you can request subset fonts via the API:

<!-- Google Fonts with subset as a parameter -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&subset=latin" rel="stylesheet" />

Step 3: Use font-display Strategies

The font-display CSS descriptor controls how a font is displayed while it loads:

  • auto: Browser default behavior (typically FOIT)
  • block: Hides text for up to 3 seconds, then uses fallback
  • swap: Shows fallback text immediately, swaps when font loads
  • fallback: Shows fallback text, swaps within a 3-second block period
  • optional: Shows fallback text, font may never load
@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter-subset.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap; /* Show text immediately with fallback */
}

@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter-Bold-subset.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

Expected behavior: Text renders immediately using the fallback font. When the custom font loads, the browser swaps it, potentially causing a reflow if the metrics differ significantly.

Step 4: Preload Critical Fonts

Preload tells the browser to fetch the font early, before it is discovered in the CSS.

<!-- Preload the primary font file -->
<link
  rel="preload"
  href="/fonts/Inter-subset.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>

Expected behavior: The font request appears early in the network Waterfall, starting before the CSS file is parsed. This can reduce font loading time by 200-400ms.

Step 5: Prevent Layout Shifts with Size-Adjust

The size-adjust and ascent-override descriptors in the @font-face rule help match fallback font metrics to the custom font, reducing Cumulative Layout Shift.

@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 105%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

Tools like the @next/font package in Next.js automatically generate these fallback metrics. For manual calculation, use the Font Style Matcher tool.

Step 6: Self-Host Google Fonts

Self-hosting removes the extra DNS lookup and connection to Google servers, improving TTFB for font requests.

# Use google-webfonts-helper to download fonts
# https://gwfh.mranftl.com/fonts

# Download Inter font files and CSS
curl -O https://gwfh.mranftl.com/api/fonts/inter?download=zip
unzip inter.zip -d static/fonts/

Expected improvement: Font TTFB drops from approximately 200ms (Google servers) to approximately 20ms (your CDN or server).

Step 7: Inline Critical Font CSS

If the above-the-fold text needs a specific font, inline the @font-face declaration in the HTML head.

<!DOCTYPE html>
<html>
<head>
  <style>
    @font-face {
      font-family: 'Inter';
      src: url('/fonts/Inter-subset.woff2') format('woff2');
      font-weight: 400;
      font-display: swap;
    }
    body { font-family: 'Inter', Arial, sans-serif; }
  </style>
</head>

Step 8: Use Variable Fonts

Variable fonts contain multiple weights and styles in a single file, reducing font requests from 3-4 files to 1.

# Subset a variable font
pyftsubset Inter-Variable.woff2 \
  --unicodes="U+0020-007F" \
  --output-file=Inter-VF-subset.woff2
@font-face {
  font-family: 'Inter VF';
  src: url('/fonts/Inter-VF-subset.woff2') format('woff2');
  font-weight: 200 900; /* All weights in one file */
  font-display: swap;
}

h1 { font-family: 'Inter VF', sans-serif; font-weight: 700; }
p { font-family: 'Inter VF', sans-serif; font-weight: 400; }

Expected result: One 45KB file replaces three separate weight files totaling 120KB.

Learning Path

flowchart LR
  A[Critical Rendering Path] --> B[Font Optimization]
  B --> C[CSS Performance]
  B --> D[Render-Blocking Resources]
  C --> E[Performance Budgets]
  
  style B fill:#4f46e5,color:#fff
  style A fill:#6366f1,color:#fff
  style C fill:#6366f1,color:#fff

Common Errors

  1. Using too many font weights: Each weight adds a separate font request. Limit to two weights (regular and bold) unless the design absolutely requires more.

  2. Not subsetting fonts: Full fonts contain thousands of characters for languages you may never use. Subsetting to Latin reduces file size by 80 to 90 percent.

  3. Using font-display: block by default: Block hides text for up to 3 seconds, causing a poor user experience. Use swap for most body text.

  4. Forgetting to add crossorigin on preload fonts: Font preloads require the crossorigin attribute because fonts are fetched from a different origin. Without it, the preload is ignored.

  5. Not matching fallback font metrics: When the custom font loads and swaps in, it can cause a layout shift if the fallback has different metrics. Use size-adjust to minimize the shift.

  6. Serving TTF or OTF fonts on the web: TTF and OTF are uncompressed and can be 2 to 3 times larger than WOFF2. Always convert to WOFF2 for web use.

Practice Questions

  1. What is the difference between FOIT and FOUT?
  2. How does font-display: swap improve perceived performance?
  3. What is font subsetting and why is it important?
  4. Why must font preload links include the crossorigin attribute?
  5. What are the benefits of variable fonts?

Answers: 1. FOIT shows invisible text while the font loads; FOUT shows fallback text immediately and swaps to the custom font when ready. 2. It renders text with the fallback font immediately, so the user can start reading without waiting for the font download. 3. Subsetting removes unused characters from the font file, reducing file size by up to 90 percent. 4. Browsers require crossorigin on font preloads because fonts are fetched from a different origin (even same-origin fonts need the attribute). 5. A single variable font replaces multiple weight/style files, reducing HTTP requests and total font weight.

Challenge

Analyze the fonts on a production site. Create a report showing each font file, its format, size, how it was loaded, and the perceived impact on text rendering. Then optimize the fonts by subsetting to Latin, converting to WOFF2, adding font-display swap, and preloading critical fonts. Measure the before and after page weight and text rendering time.

FAQ

What is the best font-display value for body text?

Use font-display: swap for body text so users can start reading immediately. Use font-display: optional for decorative fonts that are not essential to the user experience.

Can I use Google Fonts with HTTP/2 push?

Google Fonts URLs are on a different domain, so you cannot push them from your server. Self-host Google Fonts to take advantage of HTTP/2 server optimization.

How do I know which characters to include in my subset?

For English content, include Latin characters (U+0020-007F), Latin Extended-A (U+00A0-00FF), and punctuation. Add additional ranges if your content includes accented characters.

Does font optimization affect Accessibility?

Yes, using font-display: optional means fonts may never load on slow connections. Ensure the fallback font is legible and has similar metrics to the custom font.

What tools does DodaTech use for font optimization?

DodaTech uses pyftsubset (fonttools) for subsetting and woff2_compress for conversion. For Google Fonts specifically, we use the google-webfonts-helper tool to self-host optimized subsets.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro