Font Optimization for the Web

Published - 8 min read

Fonts can significantly impact web performance, often ranking as the third largest asset on a page1. This guide explores a structured approach to optimizing font loading, starting with the simplest techniques and progressing to more advanced strategies.

Overview and Principles

The core idea behind font optimization is twofold:

Reducing Work

The simplest improvements come from requiring fewer font downloads and smaller font files.

Use the user agent’s existing fonts

This is the easiest, and fastest, strategy, but doesn’t give you the control over the typography of your website.

Many CSS frameworks, like Bootstrap and Tailwind, use this strategy by default.

However, there is a lot of small details to consider when using the user agent’s existing fonts. If you’re not interested, you can just use the snippet below for a modern latin san-serif font stack:

css
body {
  font-family: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
    'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}

If you want to:

  1. Support old operating systems
  2. Support old browsers
  3. Avoid system fonts from the user’s non-latin language settings

You can use the snippet below:

css
body {
  font-family:
    ui-sans-serif,
    -apple-system,
    BlinkMacSystemFont,
    'Segoe UI',
    Inter,
    Roboto,
    Helvetica,
    Arial,
    sans-serif,
    'Apple Color Emoji',
    'Segoe UI Emoji',
    'Segoe UI Symbol',
    'Noto Color Emoji';
}

Download only the fonts you need

Limit font families

Font families are semantically named groups of fonts. For example, Inter, Aptos, Times New Roman are all font families.

By limiting the number of font families you use, you reduce the number of network requests, and the amount of bytes over the network.

Limit font styles

Font styles are the font’s weight, style, and stretch. For example, normal, italic are two different styles, and 300, 400, 700 are different weights.

On third party font sites, like Google Fonts, you can usually find the font styles you need, and only use the ones you need.

For example, instead of using the entire Inter font family,

html
<link
  href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
  rel="stylesheet"
/>

Let’s only use the normal style, and the 400 weight:

html
<link
  href="https://fonts.googleapis.com/css2?family=Inter:opsz@14..32&display=swap"
  rel="stylesheet"
/>

Prefer variable fonts

Variable fonts are a modern font format that allows you to define the font’s weight, style, and stretch as a range of values.

Following the example above, instead of using the entire Inter font family, we can use the variable font:

html
<link
  href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,100..900&display=swap"
  rel="stylesheet"
/>

This is a single network request, and a single font file, which is much faster than downloading multiple font files. On top of that, you have the full range of weights available to you.

Font subsets

Font subsets are a way to reduce the amount of bytes over the network by only downloading the characters you need.

Google Fonts by default already provides font subsetting, so you don’t need to do anything. The files will only be downloaded if the characters are used on the page.

However, if you’re thinking to preload the fonts, it is a good idea to only preload the subsets you need. You can do this by looking into the url in the href attribute of the link tag.

css
/* cyrillic-ext */
@font-face {...}
/* cyrillic */
@font-face {...}
/* greek-ext */
@font-face {...}
/* greek */
@font-face {...}
/* vietnamese */
@font-face {...}
/* latin-ext */
@font-face {...}
/* latin */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/inter/v18/UcCo3FwrK3iLTcviYwYZ8UA3.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

If you only need the latin subset, you can preload that:

html
<link
  rel="preload"
  href="https://fonts.gstatic.com/s/inter/v18/UcCo3FwrK3iLTcviYwYZ8UA3.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>

Use optimized font formats

There are many font formats, but the most common ones are woff2, woff, and ttf. All 3 are well supported, but woff2 is the smallest and fastest to download.

You can use tools like Font Squirrel or Fontmin to convert your fonts to woff2.

Advanced font subsetting

If you want to go even further, you can limit the font subsets to only the characters you need. This requires a lot of work, and is not recommended for most websites that accepts user-generated content.

But this strategy unlocks inlining fonts in the HTML, which is the fastest way to load fonts, since you avoid a server roundtrip.

Speeding up the work

Self-host your fonts

Avoid third party font providers, and self-host your fonts.

External CDNs are slower, as you have to make an additional DNS, TCP and TLS resolution to the server. Furthermore, browsers no longer share the font cache between domains, so there’s no benefit compared to self-hosting.

The only exception is if the font license prohibits self-hosting. In that case, then follow the next section.

Preconnect to the font provider

Note
Skip if you’re not using a third party font provider.

Preconnecting to the font provider allows the browser to resolve DNS, TCP and TLS to the server early, which speeds up the font download.

html
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

Preload the fonts

Preloading fonts allow the browser to fetch the fonts in parallel with the HTML, and display the page faster. Otherwise, browsers typically only start downloading fonts after the HTML has been parsed.

html
<link
  rel="preload"
  href="/myfont.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>

Advanced: Inline the fonts

For the fastest font loading, you can inline the fonts in the HTML. This requires using scripts to parse the font files, pick the characters you need, and inlining them in the HTML.

Tools such as font-spider can help you with this.

Managing Visual Disruption

Here are the two acronyms you may come across when talking about font loading:

FOIT Example

Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.

FOUT Example

Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.

Neither of these are good, and here are some strategies to reduce them.

Use font-display

The font-display CSS property is used to control the font’s display behavior.

The most frequently used value is swap, which will swap to the fallback font if the font is not available. This reduces FOIT, but not FOUT.

css
@font-face {
  font-family: my-font;
  src: url(/my-font.woff2) format('woff2');
  font-display: swap;
}

The other values are:

optional and fallback are good choices if you would like to use your default font, but aren’t too picky. You should almost never use block since you want to avoid blocking the page from being displayed.

Use fallback fonts

We mentioned that using the user agent’s existing font is the fastest way to load fonts. So we can use that as a fallback font, which will reduce FOUT as a san-serif is more likely to be stylistically closer than the default serif font.

css
body {
  font-family: my-font, ui-sans-serif, system-ui, sans-serif,
    'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}

Use a custom fallback font

Though the above strategy is good, most san-serif fonts still have different line heights, letter spacing, and other typographic properties. Layout shifts are common, and can be disorienting.

What if we pick a san-serif font and use custom font metric overrides to make it look more like the font we’re using?

This is a technique used by the Google Aurora team56, and is the best way to reduce FOUT. It is now used in tools like Next.js Font Optimizer, Astro Font and more.

Case Study

At the time of writing, I’m using the Plus Jakarta Sans font family for this blog.

By switching from TTF to WOFF2, using variable fonts and subsetting, this site reduced the font size from 399KB to 54KB (86.4% reduction). Preloading and using font-display: swap plus a custom fallback font also improved perceived performance.

TTF399KBWOFF2193KBw/ Variable72KBw/ Subsetting54KB0KB400KB

Resources

Footnotes

Footnotes

  1. Content type and file formats

  2. Extended System Fonts 2

  3. Never, ever use system-ui as the value of font-family

  4. Implementing system fonts on Booking.com — A lesson learned.

  5. Font Fallbacks

  6. Notes on calculating font metric overrides