WOFF2 (Web Open Font Format 2.0) landed in 2014 as Google's improvement over the original WOFF standard. The core innovation: replace zlib compression with Brotli, a compression algorithm Google built specifically for web assets, plus font-specific preprocessing that restructures font tables into more compressible forms before Brotli touches them. The result is significantly smaller files with zero quality loss.
Twelve years later, WOFF2 has 97.5% global browser support. Chrome, Firefox, Safari, and Edge have supported it since 2018 or earlier. IE11 is the only notable holdout, and it's been dead since 2022. The debate about which web font format to use is over. This guide covers the technical details of WOFF2 — how Brotli compression works on fonts, how to create WOFF2 files, how to serve them correctly, and the performance optimizations that actually matter.
How Brotli Compression Works on Fonts
WOFF2 doesn't just run Brotli over the raw font binary. Before compression, a font-specific preprocessor transforms the data to make it more compressible. This preprocessor knows the structure of OpenType tables and rearranges data within them so that similar bytes cluster together — which Brotli (a dictionary-based compressor) can then encode much more efficiently.
The Preprocessing Step
The WOFF2 encoder applies five transformations to font tables before compression:
- glyf table: Coordinate deltas are separated from flags, and points are stored as differences rather than absolutes. This clusters similar small values together
- loca table: Reconstructed from glyf, so it's stored as zero bytes (free compression)
- hmtx table: Left side bearings and advance widths are split into separate streams
- cmap, kern, and other tables: Reordered for better compression locality
These transformations are lossless and reversible. The decoder undoes them after decompression, producing the exact original font tables. But by structuring data for Brotli's strengths, they add 15-25% compression on top of what Brotli alone would achieve.
Real-World Compression Ratios
Typical compression results for popular fonts:
| Font | TTF | WOFF (zlib) | WOFF2 (Brotli) | WOFF2 savings vs TTF |
|---|---|---|---|---|
| Inter Regular | 316 KB | 184 KB | 132 KB | 58% |
| Roboto Regular | 168 KB | 99 KB | 68 KB | 60% |
| Open Sans Regular | 217 KB | 128 KB | 90 KB | 59% |
| Noto Sans JP Regular | 4.7 MB | 2.9 MB | 1.6 MB | 66% |
| Source Code Pro Regular | 186 KB | 110 KB | 76 KB | 59% |
CJK fonts see the highest compression ratios because their large glyf tables (thousands of complex glyphs) benefit most from the preprocessing step. Latin fonts consistently compress to 55-60% smaller than TTF.
Creating WOFF2 Files
You have three options for converting existing fonts to WOFF2. All produce identical output because the WOFF2 spec defines the encoding precisely.
Using fonttools (Python)
The fonttools Python library is the standard tool for font manipulation. Install it with Brotli support:
pip install fonttools brotli
# Convert TTF to WOFF2
python -m fonttools ttLib -o output.woff2 input.ttf --flavor woff2
# Or use the pyftsubset wrapper (also subsets)
pyftsubset input.ttf --output-file=output.woff2 --flavor=woff2 --layout-features='*'The --layout-features='*' flag preserves all OpenType features. Without it, pyftsubset strips unused features, which saves bytes but can break ligatures, kerning, and other typography.
Using Google's woff2 Tool
Google maintains a standalone C++ WOFF2 encoder/decoder:
# Install (Ubuntu/Debian)
sudo apt install woff2
# Compress
woff2_compress font.ttf # produces font.woff2
# Decompress
woff2_decompress font.woff2 # produces font.ttfThis is the fastest option for batch conversions. It's the reference implementation, so its output is the gold standard for WOFF2 compliance.
Online Conversion
For quick one-off conversions without installing tools, you can convert TTF to WOFF2 or convert OTF to WOFF2 directly in your browser. The conversion is lossless — WOFF2 is just a compression wrapper around the original font data. If you have WOFF files from an older project, you can also convert WOFF to WOFF2 for the extra Brotli savings.
@font-face CSS Setup
The minimal correct @font-face declaration for WOFF2:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
The format() Hint Matters
The format('woff2') hint tells the browser the file type before downloading it. Without the hint, the browser downloads the file and inspects its bytes to determine the format — wasting bandwidth if it doesn't support WOFF2. Always include the format hint.
For variable fonts, use format('woff2-variations') or the newer format('woff2') tech('variations') syntax. Both work, but the tech() syntax is the forward-looking standard.
Do You Need a WOFF Fallback?
The WOFF-as-fallback pattern adds a second source line:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2'),
url('/fonts/inter.woff') format('woff');
font-display: swap;
}The browser tries WOFF2 first and falls back to WOFF if it doesn't understand the format. In 2026, this fallback serves the ~0.5% of browsers that support WOFF but not WOFF2. If your analytics show zero IE11 or ancient Android traffic, WOFF2-only is fine. The fallback costs nothing except maintaining an extra file.
font-display: The Critical Property
font-display controls what happens while the font downloads:
swap— Show fallback text immediately, swap to web font when ready. Best for body text. Causes FOUT (brief flash of fallback font) but zero invisible textoptional— Use web font only if already cached; otherwise stick with fallback permanently. Zero FOUT, zero FOIT. Best for repeat visitsfallback— Short invisible period (100ms), then fallback, then swap if font loads within 3s. Compromise approachblock— Hide text up to 3 seconds waiting for the font. Causes FOIT. Almost never the right choice for body text
Use swap for body text and headings. Use optional for decorative fonts where a flash of different typography would be jarring. Never use block unless you'd rather show nothing than show the wrong font (icon fonts being the rare exception).
Preloading WOFF2 Fonts
By default, the browser discovers web fonts late — it has to download HTML, parse CSS, build the render tree, find text that needs a custom font, and then start downloading the font file. Preloading shortcuts this by telling the browser to start the download immediately:
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
Performance Impact
Preloading typically saves 200-500ms on font loading, depending on network conditions. On 3G connections, the savings can exceed 1 second. This directly improves Largest Contentful Paint (LCP) when your LCP element is text styled with a web font.
Only preload the font(s) used above the fold — usually your primary body font and possibly your heading font. Preloading every weight and style defeats the purpose by competing for bandwidth during the critical rendering path. Two preloaded fonts is practical; four is usually too many.
The crossorigin Attribute Is Required
The crossorigin attribute on the preload link is mandatory, even if the font is on the same origin. This is because fonts always use CORS fetching mode per the CSS Fonts spec. Without crossorigin, the browser makes a non-CORS request for the preload and a CORS request for the actual @font-face load — resulting in two downloads of the same file.
This is the single most common preload mistake. Check your browser DevTools Network tab: if you see the same WOFF2 file downloaded twice, you're missing crossorigin.
CORS Requirements for Cross-Origin Fonts
If your fonts are served from a different origin than your HTML (a CDN subdomain, an asset domain, or a shared font server), the font server must include the Access-Control-Allow-Origin header:
# Allow any origin
Access-Control-Allow-Origin: *
# Or restrict to your domain
Access-Control-Allow-Origin: https://yourdomain.com
Diagnosing CORS Failures
When CORS blocks a font, the browser downloads the entire file and then discards it. You'll see a successful 200 response in the Network tab followed by a CORS error in the Console. The font silently fails to render, falling back to the system font. Users see the wrong font with no visible error — this is one of the most frustrating web font bugs to diagnose.
Common scenarios that trigger CORS failures:
- CDN subdomain:
assets.example.comserving fonts forexample.com - www vs non-www: Fonts on
www.example.comrequested fromexample.com - HTTP vs HTTPS: Mixed protocols count as different origins
The fix is always on the server serving the font file: add the Access-Control-Allow-Origin response header.
Content-Type and Cache Headers
WOFF2 files should be served with the correct Content-Type and aggressive caching:
Content-Type: font/woff2
Cache-Control: public, max-age=31536000, immutable
Getting Content-Type Right
The official MIME type for WOFF2 is font/woff2. Some older servers serve application/font-woff2 or application/octet-stream. Browsers are lenient and will render the font regardless, but incorrect Content-Type can cause:
- DevTools warnings that clutter your console
- Content security policy (CSP) failures if you use
font-srcdirectives - Cloudflare and other CDNs may not apply font-specific optimizations
For other font formats: TTF is font/ttf, OTF is font/otf, WOFF is font/woff. Most web servers (Nginx, Apache, Caddy) have these mappings built in. If yours doesn't, add them to your MIME type configuration.
Cache Strategy
Fonts are static assets that rarely change. Use max-age=31536000 (1 year) with immutable to prevent revalidation requests. If you need to update a font, use cache-busting filenames (inter-v4.2.woff2) or content hashes (inter-abc123.woff2) rather than invalidating the cache.
On Cloudflare, WOFF2 files are cached by default in the CDN. On AWS CloudFront, you may need to whitelist the Origin header for CORS to work with cached responses.
WOFF2 is the web font endgame. Brotli compression with font-specific preprocessing delivers the smallest possible font files without touching glyph quality. The browser support question was settled years ago. If your site still serves TTF, OTF, or WOFF to the web, you're sending 30-60% more bytes than necessary.
The action items are concrete: convert your TTF fonts to WOFF2, convert your OTF fonts to WOFF2, add font-display: swap, preload your critical font with crossorigin, and set a one-year cache. That's the entire web font strategy.