TYPEFOUND

Sign in

Back to the home page

This is for the people who run the site.
The typefaces are on sale to everyone.

← The journal

WOFF2, TTF, OTF — which file goes where

Four extensions, two real formats, and one decision that matters more than all of them. What to serve on the web, what to install, and why the file you ship should not be the file you sell.

15 September 2026 · 5 min read · Perhapxin

A font arrives as .ttf, .otf, .woff, .woff2, and every guide tells you to use WOFF2 without saying what the others are or why they exist. Here is the short, accurate version, followed by the part that actually costs money if you get it wrong.

There are two formats and two wrappers

TrueType and PostScript/CFF are the two ways of describing an outline. TrueType uses quadratic curves; CFF uses cubics. .ttf usually means the first, .otf usually means the second, and both are OpenType — the container has been the same since 1996. Which one you have affects file size slightly and rendering hardly at all on any screen made this decade.

WOFF and WOFF2 are not formats. They are compression wrappers around one of the above. WOFF2 uses Brotli and is roughly 30% smaller than WOFF, which used zlib.

194 KBa 634-character face as TTF
62 KBthe same face as WOFF2
68%saved, for one line of build config
Measured on Typefound Quill. The outlines are identical; only the packing changed.

So the answer to "which one" is boring:

  • Serving to browsers: WOFF2. Nothing else. Support has been universal since 2018, and the fallback chain people still write for IE11 is now several kilobytes of CSS defending against a browser nobody has.
  • Installing on a machine: TTF or OTF. Both install on macOS, Windows and Linux. WOFF2 does not install anywhere — it is a web delivery format and the OS font manager will refuse it.

That is the whole of the format question. Now the part that matters.

The file you serve should not be the file you sell

Here is a real number from this site. Our largest face covers 12,201 characters — Latin, Greek, Cyrillic, Korean syllables, Japanese kana, Thai.

The whole font 160 KB 12,201 characters. What somebody buys.
What this site loads 73 KB 705 characters. Enough to set these pages.
Both are WOFF2 of the same typeface. The difference is not compression, it is subsetting.

A page that sets an English headline has no use for eleven thousand Korean syllables, but the browser downloads the file before it can know that. Subsetting is not an optimisation here; it is the difference between a font a page can use and one it cannot.

Subsetting with pyftsubset:

pyftsubset font.ttf \
  --unicodes="U+0000-00FF,U+2000-206F,U+2190-21FF" \
  --layout-features='*' \
  --flavor=woff2 \
  --output-file=font.subset.woff2

Keep --layout-features='*' unless you know which features you are dropping. Subsetting the feature tables is where kerning quietly disappears.

The trap we fell into: we subsetted for the site, then served that same subset to people who had bought the web licence. They were paying for a font with Korean in it and downloading one without. The site's copy and the customer's copy are two different files and need to be treated as two different files — ours now live in separate places so they cannot be confused again.

Loading it without the flash

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

font-display is the only property in that block that people get wrong, and it has five values that are really three decisions:

Value Block Swap In practice
auto ~3s infinite Whatever the browser feels like
block 3s infinite Invisible text, then your font
swap 0ms infinite Fallback immediately, then your font
fallback 100ms 3s Brief flash, then gives up
optional 100ms 0ms May never use your font at all

For a display face on a headline, swap is right: the text is readable instantly and the headline redraws. For body text, swap is the one that produces the jarring reflow everybody hates — optional is worth considering, because body text that renders in the fallback is a page that still works.

The reflow is fixable

The jump when the font swaps is not caused by the swap. It is caused by the fallback having different metrics. You can match them:

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

Set those so the fallback occupies the same vertical space as the real font and the swap stops moving anything. It is fiddly to derive by hand; the Fallback Font Generator and fontaine both compute the numbers for you.

Preload the one that matters

<link rel="preload" href="/fonts/quill.woff2" as="font" type="font/woff2" crossorigin>

crossorigin is required even for same-origin fonts — fonts are fetched in CORS mode, and without it the browser fetches the file twice. Preload exactly one face, the one in the first screenful. Preloading five fonts is preloading nothing.

The cache trap nobody warns you about

/fonts/quill.woff2 is the same URL whatever is inside it. Change a glyph, redeploy, hard reload — and the old outlines come back, because a hard reload refetches the page, and the font was requested by CSS the page had already parsed. It comes from the disk cache, or from the CDN edge, and you will spend an afternoon convinced your build is broken.

Put a hash of the bytes in the URL:

src: url('/fonts/quill.woff2?v=3f9a21c4') format('woff2');

Same reason bundlers hash their output. Fonts are the one asset most projects forget to do it for.

A short checklist

  1. Serve WOFF2 only. Delete the WOFF and EOT fallbacks.
  2. Subset to the characters your pages actually set — but keep --layout-features='*'.
  3. font-display: swap for display type, and consider optional for body.
  4. Override the fallback's metrics so the swap does not move the page.
  5. Preload one file, with crossorigin.
  6. Hash the URL, or you will be debugging a cache.
  7. Keep the licensed original somewhere your web build cannot accidentally serve.

That last one is not a performance point. It is the one that has your customers in it.

More from the journal

These notes are written while drawing the typefaces this site sells. They are display faces, licensed for commercial work, from $3.