Font Format Strategy: WOFF2, WOFF, TTF, and the src Fallback Chain

This guide is part of the Font Loading & Delivery Strategies blueprint. It answers a deceptively simple question that quietly wastes bandwidth on a huge number of sites: which font formats should you ship, and in what order should you list them inside @font-face { src: ... }?

Get it wrong and you serve a 200KB TTF where a 60KB WOFF2 would do, or you list formats in an order that makes modern browsers download a heavier file than necessary. Get it right and a single, correctly ordered src declaration delivers the smallest possible file to every browser while degrading gracefully on the rare old one. Start the diagnosis where it shows: open Chrome DevTools, go to the Network panel, filter by Font, reload, and read the Type/size column. If you see anything other than woff2 going to an evergreen browser, you have a format problem.

The Format Landscape: One Winner, One Fallback, Several Legacies

There are six web font container formats you might encounter. In 2026 the picture is sharp:

  • WOFF2 — the format you actually want. It wraps the same OpenType/TrueType data the other formats carry, but compresses it with Brotli, yielding files roughly 30% smaller than WOFF and far smaller than raw TTF/OTF. Supported in every evergreen browser.
  • WOFF — the previous generation, Zlib-compressed. Useful only as a fallback for genuinely ancient browsers. Almost always redundant today.
  • TTF / OTF — raw, uncompressed outline fonts. Fine as the source you subset and convert from, never what you ship for the web. They are 30–50% larger than the equivalent WOFF2.
  • EOT — Embedded OpenType, an Internet-Explorer-only format. IE reached end-of-life in 2022. Drop it.
  • SVG fonts — a long-dead format removed from Chrome and Firefox years ago and never broadly supported. Drop it.

The strategic takeaway: ship WOFF2, optionally back it with WOFF, and never ship TTF/OTF/EOT/SVG fonts to the browser. WOFF2 alone covers well over 98% of global traffic.

Font format size comparison and src format fallback chain WOFF2 is the smallest format; the browser walks the src list top to bottom and stops at the first format it understands. Relative payload (same font) WOFF2 ~60KB (Brotli) WOFF ~85KB (Zlib, ~30% larger) TTF/OTF ~120KB (uncompressed) src fallback chain (top to bottom) woff2 format woff format (fallback) Browser stops at the first format() it supports and downloads only that file. An evergreen browser never fetches the woff fallback.
WOFF2 is the smallest container; the browser walks the src list and downloads only the first format it can decode.

How the src Fallback Chain Actually Works

A single @font-face rule can list several sources. The format() hint tells the browser what each URL contains, and the browser walks the list top to bottom, stopping at the first format it supports — and only then downloading that one file. It does not download all of them. This is why ordering matters: put WOFF2 first so capable browsers grab it and stop, then WOFF as a fallback for the rare browser that cannot decode WOFF2.

src: url('/fonts/inter.woff2') format('woff2'),
     url('/fonts/inter.woff')  format('woff');

Because the browser short-circuits at the first supported entry, listing WOFF2 first costs nothing for old browsers (they skip to WOFF) and saves modern browsers from ever considering the heavier file. Reverse the order and you would feed WOFF to browsers that could have taken WOFF2 — a measurable regression. For most production sites in 2026, WOFF2 alone is sufficient and the WOFF fallback is optional belt-and-braces.

Variable Fonts Use WOFF2 Too

A variable font is not a separate format — it is an OpenType font carrying interpolation axes, and it is packaged in exactly the same containers. Always ship variable fonts as WOFF2: the Brotli compression matters even more here because a variable file embeds multiple masters and is larger than a single static weight. One variable WOFF2 covering an entire wght range routinely beats shipping four or five static WOFF2 weights on total bytes. Declare it with a format('woff2') hint and a weight range in the @font-face. There is no benefit to a TTF variable font on the web; convert it to WOFF2 in your build.

Pair Formats with Subsetting

Format choice and unicode-range subset loading are complementary, not alternatives. WOFF2 shrinks the compression dimension; subsetting shrinks the glyph-count dimension. The biggest wins come from doing both: take your source TTF, strip it to the scripts you serve, then compress to WOFF2. A full multi-script family at 300KB TTF can become a 25KB Latin-only WOFF2 subset — an order of magnitude smaller — which is why your build pipeline should always end in --flavor=woff2.

Why TTF, OTF, EOT, and SVG Fonts Are Legacy

It is worth being precise about why the older formats are dead weight, because the reasons differ:

TTF and OTF are the native desktop outline formats. They carry exactly the glyph data WOFF2 does — in fact WOFF2 is built from them — but with no web-oriented compression. Served directly, a TTF is typically 30–50% larger than its WOFF2 equivalent. They remain essential as the source you feed into pyftsubset, but linking them from a production @font-face is pure waste. Worse, if a TTF entry sits above WOFF2 in the src list, a capable browser will take the larger file and stop.

EOT (Embedded OpenType) existed solely to satisfy Internet Explorer's font-embedding requirements and was never supported by any other browser. Internet Explorer reached end-of-life in June 2022, and Microsoft's own Edge dropped IE mode's relevance for public sites. An EOT entry in 2026 serves no living browser.

SVG fonts were an early attempt to express glyphs as SVG paths. They were removed from Chrome (2015) and Firefox (2014–2015), and were only ever partially supported in WebKit for a niche on old iOS. They are larger, slower, and unsupported — a pure liability.

The migration rule that follows: keep TTF/OTF strictly as build inputs, and delete every EOT and SVG-font entry from your stylesheets. Each removed entry shrinks your CSS and removes a chance for a browser to pick the wrong file.

Compression Math: Where the 30% Comes From

WOFF and WOFF2 wrap identical font tables; the size gap is entirely about the compressor. WOFF uses Zlib/DEFLATE, the same algorithm behind gzip. WOFF2 uses Brotli, a denser general-purpose compressor, plus a font-specific preprocessing transform that reorders and re-encodes the glyph (glyf) and location (loca) tables so they compress better. The combination yields the widely cited ~30% reduction over WOFF for Latin text fonts, and often more for large CJK fonts where the transform has more redundancy to exploit.

Because that compression is baked into the file, you should not add a transport-layer Content-Encoding: br or gzip on top of a WOFF2 response — the file is already compressed, the second pass typically grows it slightly, and it burns server CPU. Serve WOFF2 with Content-Type: font/woff2 and no extra encoding. The corollary: a raw TTF would benefit from transport compression, which is one more reason the right move is simply to convert it to WOFF2 once at build time rather than compress it on every request.

Decision Matrix

Format Compression Relative size Ship to browser? Use when
WOFF2 Brotli smallest (baseline) Yes — primary Always, every evergreen browser
WOFF Zlib ~30% larger Optional fallback Only if you must support pre-WOFF2 browsers
TTF/OTF none 30–50% larger No As the source you subset/convert from
EOT none large No Never (IE-only, IE is EOL)
SVG font none large No Never (removed from browsers)
Variable WOFF2 Brotli one file, many weights Yes Multiple weights of one family

Browser Support Matrix

Format Chrome/Edge Firefox Safari Notes
WOFF2 36+ 39+ 10+ (iOS 10+) >98% of global traffic
WOFF 5+ 3.6+ 5.1+ Fallback only
TTF/OTF 4+ 3.5+ 3.1+ Don't ship to web
Variable fonts (WOFF2) 62+ 62+ 11+ Use format('woff2')
EOT IE only IE EOL 2022
SVG fonts removed removed partial/legacy Dead

Baseline Configuration

The minimum correct setup is a single WOFF2 source with an explicit format hint and a font-display value.

Baseline @font-face — WOFF2 only

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

With a WOFF fallback for legacy reach

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-latin-400.woff2') format('woff2'),
       url('/fonts/inter-latin-400.woff')  format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

Step-by-Step Workflow

Each step ends with a verification check.

Implementation Steps:

  1. Audit what you currently serve. In DevTools Network, filter to Font and record the format and size of every font request. Verify: you have a list flagging any non-WOFF2 files reaching evergreen browsers.
  2. Locate the source. Get the original TTF/OTF master (the open-source repo or your foundry license). Verify: the source contains every weight/style you render.
  3. Subset, then convert to WOFF2. Run pyftsubset Inter.ttf --output-file=inter-latin-400.woff2 --flavor=woff2 --unicodes="U+0000-00FF,U+2000-206F" --layout-features=kern,liga --no-hinting. Verify: output is WOFF2 and under budget (target < 50KB/subset).
  4. Optionally also emit WOFF. Re-run with --flavor=woff only if you must support pre-WOFF2 browsers. Verify: the WOFF file is ~30% larger than the WOFF2 — confirming the format difference.
  5. Order src correctly. In each @font-face, list WOFF2 first, WOFF second, each with the right format() hint. Verify: in DevTools, an evergreen browser downloads only the .woff2; the .woff is never requested.
  6. Set headers and content-hash. Serve fonts with Cache-Control: public, max-age=31536000, immutable and hashed filenames. Verify: a repeat navigation reads the font from disk cache.
  7. Re-measure. Run Lighthouse and re-check the Network panel. Verify: every font reaching a modern browser is WOFF2, and total font bytes dropped versus the audit in step 1.

Auditing Your Current Formats

Before changing anything, quantify the problem. A one-line audit in the DevTools Console lists every font the page actually downloaded, with its transfer size, so you can spot non-WOFF2 stragglers:

List downloaded fonts and sizes from the Console

performance.getEntriesByType('resource')
  .filter(r => r.initiatorType === 'css' && /\.(woff2?|ttf|otf|eot)(\?|$)/.test(r.name))
  .forEach(r => console.log(
    r.name.split('/').pop(),
    Math.round(r.transferSize / 1024) + 'KB'
  ));

Any .ttf, .otf, .eot, or .svg in that output reaching a modern browser is a format defect. Any .woff downloaded alongside a .woff2 for the same face usually means an src ordering bug — the browser should never need both. Capture this list before and after your migration to prove the byte savings; pairing it with PerformanceResourceTiming lets you track the same metric in the field, not just on your machine.

Common Pitfalls

  • Shipping TTF to the browser. A raw TTF is 30–50% larger than its WOFF2 equivalent for identical glyphs. Convert it; never link a .ttf from @font-face in production.
  • Listing WOFF before WOFF2. The browser stops at the first supported format, so a WOFF-first order feeds the heavier file to browsers that could have taken WOFF2.
  • Missing or wrong format() hints. Without the hint some browsers must sniff the file; a wrong hint (e.g. labeling a WOFF2 as woff) can make a browser skip a file it actually supports.
  • Still shipping EOT/SVG fonts. These add dead weight for browsers that have been gone for years. Remove the entries entirely.
  • Converting variable fonts to static TTF. This throws away the whole point of one file covering many weights and uses the heavier format. Keep variable fonts as WOFF2.
  • Compressing twice. WOFF2 is already Brotli-compressed; serving it with server-side gzip/Brotli on top wastes CPU and adds nothing. Serve WOFF2 with the right MIME type and no extra content-encoding.

Frequently Asked Questions

Do I still need a WOFF fallback in 2026?

For nearly all sites, no. WOFF2 is supported by every evergreen browser and covers well over 98% of global traffic, including Safari since version 10. Add a WOFF fallback only if your analytics show a meaningful audience on pre-2016 browsers; otherwise a single WOFF2 source keeps your @font-face rules simpler and your build smaller.

Why is WOFF2 smaller than WOFF if they wrap the same data?

The difference is the compression algorithm. WOFF uses Zlib (DEFLATE); WOFF2 uses Brotli plus a font-specific transform that reorders the glyph tables for better compressibility. The net result is roughly 30% smaller files for the same outlines, which is why WOFF2 is the default for the web.

Should variable fonts be a different format from static fonts?

No. A variable font is an OpenType font with extra axis data and ships in the same WOFF2 container. Always use format('woff2') for variable fonts — Brotli's gains are even more valuable because variable files are larger, and one variable WOFF2 usually beats several static WOFF2 weights on total bytes.

Can I serve fonts gzipped to make them smaller?

Not usefully for WOFF2 — it is already Brotli-compressed internally, so an extra transport-layer compression pass yields almost nothing and wastes server CPU. Serve WOFF2 with Content-Type: font/woff2 and no additional Content-Encoding. (Raw TTF would benefit from transport compression, but you should not be serving raw TTF anyway.)

Related