Font Optimization for the Web
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:
- Reduce Work: Minimize the amount of font data that must be downloaded and parsed.
- Speed Up Work: Use techniques to fetch and apply fonts as early and efficiently as possible.
Reducing Work
The simplest improvements come from requiring fewer font downloads and smaller font files.
Use the user agent’s existing fonts
- Network requests are slow, so the fewer bytes we need to download, the faster the page will load.
- A user agent’s existing fonts are the fonts that are installed on the user’s device which we can use without having to download them.
- We skip making network requests, which is the fastest way to load 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:
body { font-family: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; }
ui-sans-serif
is the default sans-serif font on the operating system, but may not map to anything, and is currently only supported on Safari2.system-ui
is the default font on the operating system, which can be overridden by the user. It may not be a san-serif font, and changes by language. May cause issues for very old operating systems3.sans-serif
is the default sans-serif font on the browser, which can be overridden by the user2.- The last 4 fonts are fallback fonts to support emojis on various operating systems.
If you want to:
- Support old operating systems
- Support old browsers
- Avoid system fonts from the user’s non-latin language settings
You can use the snippet below:
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'; }
-apple-system
uses the default font in very old versions of iOS/macOS, wheresans-serif
is not supported. May cause issues for very old operating systems4.BlinkMacSystemFont
uses the default font in very old versions of Chrome on iOS/macOS.
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,
<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:
<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:
<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.
/* 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:
<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
NoteSkip 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.
<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.
<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:
- Flash of Invisible Text (FOIT): The visual disruption caused by the page being displayed before the fonts are loaded.
- Flash of Unstyled Text (FOUT): The visual disruption caused by fonts changing, causing layout shifts and disconnesence.
FOIT Example
FOUT Example
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.
@font-face { font-family: my-font; src: url(/my-font.woff2) format('woff2'); font-display: swap; }
The other values are:
auto
: The browser will use the font’s display behavior.block
: The browser will wait for the font to be loaded before displaying the text.fallback
: The browser will block for an extremely short time, and a short swap period.optional
: The browser will use the font if it is available, otherwise it will use the fallback font.
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.
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.