How to Convert Fonts for the Web and Cut Their Size by Two Thirds

Custom fonts are one of the heaviest things a page loads, and they load on the critical path: until the font arrives, the browser is deciding whether to show your text at all. Getting this right is one of the more reliable performance wins available, and it takes about ten minutes.

This guide covers the whole path - converting the file, writing the CSS, and the two mistakes that account for most bad font loading on the web.

Convert to WOFF2, and stop there

WOFF2 wraps the same font data in Brotli compression with some font-specific preprocessing. It is not a different font; it is your font, packed properly. Real numbers from Noto Sans Mono, a 3,787-glyph family:

  • TTF - 510,612 bytes
  • WOFF - 254,600 bytes, half the original
  • WOFF2 - 175,192 bytes, roughly a third

Because it is only a container change, it is completely lossless. Converting to WOFF2 and back gave back all 3,787 glyphs byte-for-byte identical when we tested it. There is no quality trade-off to weigh here.

Convert what you have: TTF to WOFF2, OTF to WOFF2, or WOFF to WOFF2 if you were given the older web format.

The advice to also ship WOFF, TTF and EOT fallbacks is left over from around 2015. WOFF2 works in every current browser and has for years. Serving four formats today means a more complex build and more bytes in your CSS for an audience that has effectively gone.

Write the @font-face rule

The minimum that is actually correct:

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

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

body {
  font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
}

Two things there deserve explanation, because they are the parts people get wrong.

Mistake one: letting the browser hide your text

font-display: swap is the single most valuable line in that block. Without it, most browsers hide text for up to three seconds while they wait for the font - the flash of invisible text. A visitor on a slow connection stares at a blank layout while the content is sitting there, already downloaded.

With swap, the browser renders immediately in your fallback font and swaps in the web font when it arrives. Text may visibly change, which is a much smaller problem than text that is not there at all. If the reflow bothers you, font-display: optional goes further: the browser uses the web font only if it arrives almost instantly, and otherwise sticks with the fallback for that visit.

Mistake two: declaring weights you did not load

Each @font-face block maps one file to one weight and one style. If you declare only a 400 weight and then write font-weight: 700 somewhere, the browser will not find a bold file - it will synthesise one by algorithmically smearing the regular weight. The result is the slightly mushy fake bold you have seen on many sites, and it happens silently.

Either load a real bold file with its own @font-face block, or do not use bold in that family. The same applies to italics, where synthetic slanting looks noticeably worse than a real italic, because a designed italic is usually a different set of letterforms rather than the upright ones tilted over.

Load fewer weights than you think you need

Every weight and every style is a separate file on the critical path. Regular and bold covers the large majority of interfaces. A medium for headings is a reasonable third. Beyond that you are spending real load time on distinctions most readers will not consciously register.

Two more things that pay for themselves:

  • Preload the fonts used above the fold. A single <link rel="preload" as="font" type="font/woff2" crossorigin> for your body font starts the download before the CSS has been parsed. Note that crossorigin is required even for same-origin fonts, and the preload is silently wasted without it.
  • Self-host rather than using a font CDN. Since browsers partitioned their HTTP caches, a third-party font is no longer shared between sites, so the old argument for a CDN is gone. Self-hosting removes a DNS lookup and a connection from the critical path, and it stops a third party from seeing your visitors.

Subsetting, if you want the last big win

A full font carries glyphs for languages your site will never render. A Latin-only subset of a font that also covers Cyrillic and Greek can be a fraction of the size, and this is often a bigger saving than the WOFF2 conversion itself.

Google Fonts does this for you through the &text= and subset parameters. For a self-hosted font, the standard tool is pyftsubset, part of fontTools:

pip install "fonttools[woff]" brotli

pyftsubset inter-regular.ttf \
  --unicodes="U+0000-00FF,U+2010-2027,U+20A0-20BF" \
  --layout-features="kern,liga" \
  --flavor=woff2 \
  --output-file=inter-regular.subset.woff2

One caution: subset aggressively and a visitor's name, a quoted passage, or a currency symbol you did not anticipate will fall back to a different font mid-sentence. Basic Latin plus punctuation plus the currency block is a sensible floor for an English-language site.

Going the other way

Sometimes you need the reverse - you have a web font and need something installable, perhaps to open a design file or match a brand font locally. Web formats do not install on any operating system, so unwrap them first with WOFF2 to TTF or WOFF2 to OTF.

Choose the output that matches the outlines inside. If the font was originally a PostScript-flavoured OTF, ask for OTF; if it was TrueType, ask for TTF. Requesting the other one still works, but it converts the outlines rather than just unwrapping them, which is a redraw. The difference is explained in TTF vs OTF vs WOFF vs WOFF2.

Check the licence before you publish

This is the step that gets skipped, and it is the one with consequences. Converting a font grants you no rights you did not already have. A large share of commercial fonts are sold with a desktop licence covering your own machine, and web embedding requires a separate licence that is frequently priced by monthly pageviews. Foundries do check, and a web font sitting in a public directory is trivially easy to find.

Before publishing, read the licence that came with the font, look for the words "web font" or "embedding", and if the font came from an unclear source treat that as a no. The open families are genuinely excellent now, and anything under the SIL Open Font Licence can be self-hosted and converted freely.

A related point on handling: a purchased font is an asset you are usually not permitted to redistribute. Conversions here run entirely in server memory, never touch a disk, and the file is discarded as soon as your download is sent, so no copy of a licensed font remains on our side afterwards.

Web font converters:
TTF to WOFF2 OTF to WOFF2 WOFF to WOFF2 WOFF2 to TTF All font tools