A deep dive into Webfonts
Tags: webdev, frontend, ux, css
Published: 2020-07-06 07:47:38

(Originally posted on my blog: https://iamschulz.com/a-deep-dive-into-webfonts/)

Inspired by Ben Halperns [discussion about webfonts](https://dev.to/ben/what-s-the-most-efficient-way-to-load-custom-fonts-on-the-web-2ni3), I decided to take a deep dive into the subject. What are Webfonts, where did they come from and how do we load them as fast as possible?

# TL;DR
- convert your font to woff/woff2
- split font files into character sets and load them with `unicode-range`
- preload your font or use HTTP2 server pushes
- use `font-display: fallback;` or `optional;`
- Cache your fonts efficiently

# What's a font?
Let's start at the beginning: What are fonts?

A font file is a collection of vector-based graphics. That means they're scalable, so you don't need a different set of graphics for different sizes. Those graphics get assigned to addresses, which represent specific glyphs. A glyph can be 
- a letter (`A`)
- a number (`3`)
- a mathematical operator (`÷`)
- weird dingbats (`♆`)
- or whatever else you can find in the [Unicode table](https://unicode-table.com).

Partly because font files would become bloated, partly because it's a huge amount of work, next to no font supports the full Unicode set. We mostly work with subsets and accept the constraints that come with that. If you're a non-English speaker and your language uses its own letters, you'll most certainly have been in the situation of having found a nice font, but unable to use it, because there's no `ß` inside. I'll [cover later](#synthetic-fonts) on how the browser handles those cases.

Fonts can also transform their characters on a contextual basis with ligatures. A common example would be `fi`, with the arch of the `f` merging with the `i`'s dot and the x-line serifs becoming one. 

![The lowercase letters fi without and with a ligature, merging the f's arch with the i's dot.](https://dev-to-uploads.s3.amazonaws.com/i/vkwf0ysjj03g35awn4ht.png)

The font recognizes defined character strings and replaces them with a special graphic. That mechanism can be taken to an extreme [for creative effects](https://vole.wtf/scunthorpe-sans/).

A font file typically includes only one style (as in bold, italic, etc). A font family that covers all styles of a font consists of multiple font files. This becomes very important when bringing fonts into the web because each style means another request.

# Formats
Back in the days of the [Browser Wars](https://en.wikipedia.org/wiki/Browser_wars), you had to have a plethora of file formats to get your webfont running consistently. There was EOT for Internet Explorers, SVG for older iOS devices, and TTF and OTF for more modern browsers like Chrome and Firefox. 

Things have become clearer since then. TTF and OTF were pioneered by Microsoft, who also introduced the `@font-face` rules to begin with. Both are flavors of OpenType, with TTF being focused on TrueType and OTF being focused on PostScript. There's a comprehensive history in the [Wikipedia article](https://en.wikipedia.org/wiki/OpenType). OpenType eventually got a nice, browser-supported compression and evolved into woff (which became woff2 with an even better compression).

[Browser support](https://caniuse.com/#search=woff) for woff and woff2 is very good. You can pretty much just stick to them and forget about the others unless you have to support some niche devices. Fonts that are only available as TTF/OTF can [easily be converted](https://onlinefontconverter.com/) to woff2.

Still, what's even better than loading a super optimized font over the network is checking if the user already has it on their system. When using very popular fonts like *Open Sans*, *Roboto* or *Fira Code* the chance of that is quite high. Let's put the `local()` option at the top of the list.

# Loading a webfont
A naïve implementation of a webfont would look like this:
```html
<!-- index.html -->
<!DOCTYPE html>
<html>
    <head>
        <link rel="stylesheet" type="text/css" href="./styles.css">
    </head>
    <body>Trololo</body>
</html>
```
```css
/* ./styles.css */
@font-face {
    font-family: "MyAwesomeFont";
    src: local("MyAwesomeFont"),
         url(./MyAwesomeFont.woff2) format("woff2"),
         url(./MyAwesomeFont.woff) format("woff");
}

body {
    font-family: "MyAwesomeFont";
}
```
That works. The body text is shown in the specified font. Mission accomplished, good job everyone. *Hold up!* There's still a lot of potential here. Here's what that would look like as a simplified [waterfall diagram](https://www.keycdn.com/blog/waterfall-analysis) with totally made up numbers.

![A waterfall diagram showing the document being loaded in 19ms, which triggers the CSS being loaded in 36ms, which triggers the font being loaded in 59ms.](https://dev-to-uploads.s3.amazonaws.com/i/r5l7rlokgwuti56beok7.png)

You might notice a "flash" or layout change when loading the site. There's also some heavy data usage or a bit of delay until the page has finished rendering. You can speed things up. You can cut down on both the download's size and efficiency. Let's look into that!

# Load only what you need
Before I talk about cutting down on file sizes, I'll talk about what you don't want to throw overboard.

## Font Stacks
Webfonts are progressive enhancements. A browser will always display text, whether you specified a valid font or not. It will just fall back to the system font if all else fails. The `font-family` property gives us the option to specify more than one font: That's called a font stack and it's read from left to right.
```css
.sans {
    font-family: "Roboto", "Arial", "Helvetica Neue", "Helvetica", sans-serif; 
}
```
If the webfont *Roboto* fails to load or parse for some reason, the browser falls back to *Arial*. If that's not found locally, it tries *Helvetica Neue*, then *Helvetica*, and finally uses whatever sans-serif font is specified in the browsers settings.

Here are some more stacks for serif, monospace and [fabulous](https://modus.medium.com/consider-the-lobster-75929aa299f6) fonts:
```css
.serif {
    font-family: "Roboto Slab", "Georgia", "Times", "Times New Roman", serif; 
}

.mono {
    font-family: "Fira Code", "Consolas", "monaco", monospace; 
}

.script {
    font-family: "Lobster", "Brush Script MT", cursive; 
}
```

## Synthetic fonts
If you use bold and italic styles of your font, make sure to load the corresponding files. If you use exotic characters, make sure those are included in your font.

You can have the browser synthesizing missing styles, but doing that is a great way to make designers cry. A faux-bold font is simply the base font with thicker lines. Most of the time, your text will be prettier in a *designed* font, since not all lines should scale equally with boldness and not all lines slant equally for italic fonts. This example uses [*Raleway*](https://fonts.google.com/specimen/Raleway) in Regular, Black and Italic:

![The difference between a webfont and a synthetic font](https://dev-to-uploads.s3.amazonaws.com/i/3wvdk9kxitnn2956y9p6.gif)


Missing characters are even more noticeable. They'll simply fall back according to the font stack and stick out like a sore thumb. This example uses the following font stack:
```css
font-family: "Bentham", "Comic Sans MS", sans-serif;
```

![The character Ü gets synthesized, ß and Cyrillic characters render in Comic Sans](https://dev-to-uploads.s3.amazonaws.com/i/8eozhjl89jo4xjcnwpos.jpg)


Umlauts can easily be synthesized by simply adding some graphics to a base character. Ligatures are harder, as they're completely new characters. The browser can't know how they're supposed to look like, so it falls back down the font stack. In this case, the next viable option is Comic Sans. The same goes for an entire character set that the font doesn't include. (Yes, I specifically looked for a font without Cyrillic and `ß`.)

In the end, synthetic fonts should *only* be used as a fallback option, in case your font file fails to load properly, and for the [time between loading the document and rendering the font](#content-flashes).

# Throw away what you don't need
## Trimming file sizes
Using webfonts comes at a cost: they're huge. Vector-based graphics become larger the more details they have and fonts are no exception. This is what [Roboto](https://fonts.google.com/specimen/Roboto)'s lowercase `a` looks like with all its anchor points:

![A Roboto lowercase a glyph with all its vector points](https://dev-to-uploads.s3.amazonaws.com/i/dszkvv990lnh3nei6uvn.jpg)

Doesn't seem so bad on its own, but there are 1109 glyphs inside the font. In the end that stacks up to 168kB - and that's for the regular style *only*!

### Removing characters from the file
In most cases, you don't want to simplify the glyph's paths to save space. That would come at the expense of the readability of the font - which is likely why you want to use a webfont in the first place. But you can cut down on the number of glyphs inside the font file. If you don't write exclusively in English, you can safely remove any umlauts `Ä`, `Ø`, non-decorative ligatures `ß`, `Æ` and even whole character sets like [Cyrillic](https://en.wikipedia.org/wiki/Cyrillic_script). [FontForge](https://fontforge.org/) is a simple and capable tool for that that also brings a CLI and [scripts](https://fontforge.org/docs/scripting/scripting.html), so you can automate your character stripping.

### Unicode ranges
Let's take this a step further: You've got a multi-language site. English speakers should get a font with Latin characters, Russian speakers one with Cyrillic characters and Japanese speakers one Japanese characters. It's overkill to have every user load ever character set. Enter `unicode-range`. This is a CSS property that defines what font file will be used based on your content.

```css
/* ./styles.css */
@font-face {
    font-family: "MyAwesomeFont-latin";
    src: url(./MyAwesomeFont-latin.woff2) format("woff2"),
        url(./MyAwesomeFont-latin.woff) format("woff");
    unicode-range: U+0020—007F;
} 

@font-face {
    font-family: "MyAwesomeFont-cyril";
    src: url(./MyAwesomeFont-cyril.woff2) format("woff2"),
        url(./MyAwesomeFont-cyril.woff) format("woff");
    unicode-range: U+0400–04FF;
} 

@font-face {
    font-family: "MyAwesomeFont-jp";
    src: url(./MyAwesomeFont-jp.woff2) format("woff2"),
        url(./MyAwesomeFont-jp.woff) format("woff");
    unicode-range: U+3000-9FFF, U+ff??;
} 
```
If the browser sees some Japanese characters on the page, it will now download the pre-split Japanese part of your font.

Using a modern file format and stripping out unneeded stuff leaves us with something like the following waterfall:

![A waterfall diagram showing the document being loaded in 19ms, which triggers the CSS being loaded in 36ms, which triggers the font being loaded in 12ms.](https://dev-to-uploads.s3.amazonaws.com/i/wz0gahfvbuvaviffzg4j.png)


## Loading strategies
### Preloading
Now that you've slimmed down the transfer size of our font files, it's time to polish the loading order. Let's take a look at our naïve implementation from above: The first thing the browser sees is the document. The document links to the stylesheet, so that gets loaded next. The stylesheet finally links to the font files. You can skip one step by [telling the browser directly in the document](https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content), which font files it's gonna need.

```html
<!-- index.html -->
<link rel="preload" href="./MyAwesomeFont.woff2" as="font" crossorigin>
<link rel="stylesheet" type="text/css" href="./styles.css">
```
```css
/* ./styles.css */
@font-face {
    font-family: "MyAwesomeFont";
    src: url(./MyAwesomeFont.woff2) format("woff2"),
         url(./MyAwesomeFont.woff) format("woff");
}

body {
    font-family: 'MyAwesomeFont';
}
```

Note that Preload can't know if your browser needs a woff or woff2-file, so it would just download both. All browsers that support preloading also support woff2, so we'll preload only that. The woff file stays as a fallback in CSS. 

That keeps the CSS together in its own file, isn't render-blocking in any way, and cleans up the waterfall diagram just the same. 

![A waterfall diagram showing the document being loaded in 19ms, which triggers CSS and font being loaded simultaneously.](https://dev-to-uploads.s3.amazonaws.com/i/dz5u0p7dn22yn6nteezs.png)


Great Success? Well, Firefox doesn't like it. This feature is behind a flag right now, and it seems like they're shipping it [later this year](https://bugzilla.mozilla.org/show_bug.cgi?id=1222633). Until then, browser coverage is at [around 80%](https://caniuse.com/#search=preload).

### HTTP/2 Push
But there's a similar solution that [works on Firefox](https://caniuse.com/#search=http2): Pushing resources by [HTTP/2](https://en.wikipedia.org/wiki/HTTP/2_Server_Push). The idea is similar to HTTP preload (you push any needed assets as early as possible), but a bit more extreme. The browser doesn't have to wait for the document to read the preload meta but gets the assets with the first answer from the server.

![A waterfall diagram showing the document being loaded in 19ms, with the CSS and font being loaded simultaneously to it.](https://dev-to-uploads.s3.amazonaws.com/i/gtrox32cwnnw92cv9fve.png)

Implementation is a bit trickier. Instead of just inserting a meta tag, you need to edit your [server](https://ops.tips/blog/nginx-http2-server-push/) or [CDN](https://www.cloudflare.com/website-optimization/http2/serverpush/) configuration. Also, you'll lose access to `unicode-range`, so you'll need to workaround that server-side. Still, HTTP/2 push goes as my recommendation in terms of optimizing the request chain.

## Content Flashes
You now have shortened the loading time, but font rendering still relies on a second request, which can take a long time on slow connections or fail. What happens in the meantime between the [First Paint](https://web.dev/first-meaningful-paint/) and font rendering is either of
- a *Flash of Unstyled Content* (FOUT)
![A website loading with the font changing after the site is already visible](https://dev-to-uploads.s3.amazonaws.com/i/4vab3k2r34nnt0r4pyzp.gif)

- or a *Flash of Invisible Content* (FOIT) 
![A website loading with the font nor rendering after the site is already visible](https://dev-to-uploads.s3.amazonaws.com/i/0bs1iyn4cu89qe1i5m05.gif)

Luckily, you can step in here and tell the browser what to do with the CSS property `font-display`. It takes one of five values:
- `auto`: That's the browsers default behavior. Chrome uses `swap` here, Firefox and Safari use `block`.
- `block`: The browser blocks some space for the text, but doesn't render it for a long time (3s for Chrome) unless the webfont loads. If the font didn't load in that time, the browser uses the fallback font, but will still use the webfont as soon as it loads. A long FOIT occurs with the possibility of a FOUT further down the line.
- `swap`: The browser renders the text immediately with the fallback font. As soon as the webfont loads, the text is re-rendered with that. A short FOUT occurs at that point.
- `fallback`: The browser blocks the font rendering for a short time (100ms for Chrome) while waiting for the webfont. If it doesn't load in time, the text will render in the fallback font. The browser will now wait for a long time (3s for Chrome) for the webfont. A short FOIT occurs with the possibility of a FOUT within the next 3 seconds.
- `optional`: This is the most performance-driven option. Like before, the browser blocks the font rendering for a short time (100ms for Chrome), while waiting for the webfont, then switch to the fallback font and terminate the webfonts request. A short FOIT occurs.

A short FOIT is most likely the preferable option. A long one (like when using `block`) prevents the user from reading your text at all. A FOUT should be prevented, or at least contained to a short time frame after the document renders, because it can [disrupt the user](https://web.dev/cls/) when they're already reading the text. Also, keep in mind that each re-render of a webfont is a costly [layout operation](https://developers.google.com/web/fundamentals/performance/rendering/#the_pixel_pipeline).

## Caching
Phew. Now you've gone through so much trouble to make sure your fonts load as fast as possible, let's make sure your users wont have to load them again next time they visit your page. As fonts are pretty much static in nature, we can be sure of them being still the same file at that point. Give them a long cache halftime by setting its [Cache Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) to `Cache-Control: public, max-age=31536000` and giving it an [ETag](https://developer.mozilla.org/de/docs/Web/HTTP/Headers/ETag). Also, make sure you don't apply any cachebreakers to your fonts in your budler or html.

And that's your webfont strategy done! Congratulations for making it this far and enjoy your *blazing fast*™ fonts! If you want do dive even deeper, here are some more hints on where to go:

# Variable fonts
Despite being in the spec since TTF, those are a quite recent addition to webfonts, becoming supported widely in 2018. Right now, browser support [is decent](https://caniuse.com/#feat=variable-fonts) and safe for modern clients.

Variable fonts have custom properties. Those can be font's weight, slant, width or ascender height, or completely wild and creative properties. The value of those properties is set in a range. Unlike font-weight (set in increments of 100) or slant (set as true or false), you can set them freely between a minimum and maximum value. The font reacts by morphing its vectors along pre-defined paths, manipulating the glyphs in the process. (Bonus points: [they're animatable](https://safaririot.com/)!)

That's exciting because it allows you to just load one font file and get all the weights and styles you want from that, instead of loading multiple files for a whole font family. However, variable font files are larger than their traditional counterparts. If you only use one or two font files to begin with, switching to a variable font probably isn't worth it.

# Google fonts
An honorary mention goes to [Google](https://fonts.google.com/). Their fonts collection is very extensive and actually quite fast. Harry Roberts wrote [an awesome article](https://csswizardry.com/2020/05/the-fastest-google-fonts/) on how to speed them up even more.

Google Fonts still raises some privacy concerns and gets blocked way faster by adblockers than self-hosted solutions.

# Further reads
- Almanac entry for fonts: https://css-tricks.com/almanac/properties/f/font/
- More font stacks: https://css-tricks.com/snippets/css/system-font-stack/
- A Smashing article on HTTP/2 pushes: https://www.smashingmagazine.com/2017/04/guide-http2-server-push/
- Variable fonts playgrounds: 
  - https://v-fonts.com/
  - https://www.axis-praxis.org/
  - https://play.typedetail.com/