Hello Sunil
web-font-optimization-feature-image

A Guide to Web Font Optimization

Practically every modern new website gracefully relies on Custom Webfonts. Web-fonts are fonts that are not available in the browser by default. They are fonts that are fetched remotely and rendered by the browser.

Web fonts allow you to build a much better web experience, but these fonts also cause a lot of problems such as invisible text, slow loading time, blocked view and layout shifts.

Web font optimization is a complex topic and there are many different ways to optimize the delivery of fonts.

Exactly how you optimize your fonts will depend on your hosting preferences, your site’s design and server, your technical abilities, and how far you are willing to go to improve the performance of your fonts.

Font Loading

Fonts are typically important resources, as without them the user might be unable to view page content. Thus, best practices for font loading generally focus on making sure that fonts get loaded as early as possible. Particular care should be given to fonts loaded from third-party sites as downloading these font files requires separate connection setups.

If you are unsure if your page’s fonts are being requested in time, check the Timing tab within the Network panel in Chrome DevTools for more information.

Chrome devtools network tab

Font Loading Strategies: FOIT and FOUT

Flash Of Invisible Text(FOIT) and Flash Of Unstyled Text(FOUT) are two font loading strategies used in major browsers. But what is the difference between both of them?

Custom web fonts have been around for a while now but sadly web browsers still don’t have an optimal way of loading them. Web fonts are usually large files and they take quite some time to load on your web page.

FOIT shows an invisible text while FOUT uses a system font until the font is loaded.

Different browsers have different ways of handling this delay. While some browsers would show a system font pending when the custom font gets loaded (Flash Of Unstyled Text – FOUT), some browsers would show blank text until the font has been loaded (Flash Of Invisible Text – FOIT).

BrowserDefault behavior
EdgeUses system font until the custom font is ready (FOUT)
SafariHides text until the custom font is ready (FOIT)
FirefoxInitially hides text for 3 seconds. Uses system font after that until the custom font is ready (FOIT & FOUT)
ChromeInitially hides text for 3 seconds. Uses system font after that until the custom font is ready (FOIT & FOUT)

Use The Correct Font Format

There are a number of font formats you can use (eot, ttf, woff, svg, woff2). How do you know which one to use? That’s simple, at the moment you really only need to support woff and woff2 .

Woff is a font format developed by Google. Woff is already zipped by default, which is partly why it is faster and better than all previous formats. Woff is supported by all modern browsers. Woff2 is a faster, smaller and even better version of the woff format.

Avoid Invisible Text When Loading

How to avoid FOIT?

It is quite simple and straightforward is to add the font-display: swap rule to our CSS. Such that when you want to setup your custom font, we tell the browser to load a system font and swap it out when our font is done loading.

Here’s how that would look like:

@font-face {
  font-family: Poppins;
  font-display: swap;
}

The font-display strategies that work well for traditional web fonts don’t work nearly as well for icon fonts. If possible, it’s best to replace icon fonts with SVG.

Newer versions of popular icon fonts typically support SVG. For more information on switching to SVG, see Font Awesome and Material Icons.

Specify The Unicode Range

There are thousands of unicode characters such as 👽 (& # 128125;) that can be used by the browser.

The unicode range tells a browser which characters (glyphs) the webfont contains. Remeber, at this point during page load, the font has not been downloaded yet.

By including the unicode range, a browser knows which font should be downloaded for each character.

With no unicode range delared, a browser will try all candidate fonts from top to bottom to check if this character appears in the font.  You can declare a Unicode range for yourself with the transfonter tool.

Use Font Synthesis

Lets, for a minute, assume your webpage does something similar to this setup: For normal text your page uses font-weight: 400 and font-weight: 700 for headings. Those means there are 2 fonts to load. At about 20/30Kb per font that should not be an issue.

But what if there is an italic and bold word in the text? Then, you should add 2 extra fonts to your webpage. Font-weight: 600 for bold text and font-style: italic for italc text. Your fontface set will then just become twice as big making the total font size about 80/120kb.

Font synthesis example for better font optimization

Font synthesis example

Luckily there is an alternative. If you prefer to, the browser can recreate the italic and bold font by using font synthesis . Font synthesis derives italic and bold text from normal (400) text. Not ideal of course for a designer but a technically smart solution and in some cases a good trade-off.

Shrink Fonts With Font Subsetting

Let’s take this one step further. Think about whether you really need all the characters in a font set.

For example the ‘Ç’. Do you really need it in your headings? Is the answer no? Then you could create a font subset with only the glyphs you need headings via this site font generator.

The unicode-range descriptor in the @font-face declartion informs the browser which characters a font can be used for.

@font-face {
    font-family: "Open Sans";
    src: url("/fonts/Openegular-webfont.woff2") format("woff2");
    unicode-range: U+0025-00FF;
}

Some font providers may provide different versions of fonts files with different subsets automatically. For example, Google Fonts does this by default:

/* devanagari */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/poppins/v20/pxiEyp8kv8JHgFVrJJbecnFHGPezSQ.woff2) format('woff2');
  unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
}
/* latin-ext */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/poppins/v20/pxiEyp8kv8JHgFVrJJnecnFHGPezSQ.woff2) format('woff2');
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/poppins/v20/pxiEyp8kv8JHgFVrJJfecnFHGPc.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Set The Correct and Fastest Font Order

The order of the fonts in the src attribute can make a world of difference. A browser searches for fonts from top to bottom. When a browser can’t find a (local) font or a font  type is not supported, then a browser moves on to the next font.

The fastest order is as follows: First locally search locally via local (‘myfont’). Then the woff2 (remember, this is smaller than woff) and only then the woff.

This ensures that we always use the fastest variant of the font. By the way, don’t confuse local () with the local browser cache, local looks for locally installed fonts. Often phones already have quite a few fonts pre-installed.

@font-face {  
   font-family: 'myFont';  
   font-weight: 400;  
   font-style: normal;  
   font-display: swap;
   unicode-range: U+000-5FF 
   src: local('myFont'),
        url('/fonts/myFont.woff2') format('woff2'),
        url('/fonts/myFont.woff') format('woff');
}

Give Precedence to local() in src List

Also, While you would expect a site visitor to have web safe fonts pre-installed on their computer, it’s impossible to predict whether they already have a particular webfont.

For instance, we have dozens of webfonts installed on our computer, so when we visit websites it doesn’t make sense that we should have to download a webfont we already have.

Another drawback is that a blank space (FOIT) or unstyled text (FOUC), is displayed as the font is loaded into the browser. This is absolutely unnecessary for users who already have the font locally installed on their computer.

The way to get around this is quite simple: use local() to check if a font is already on the user’s system. Listing local(‘Font Name’) first in your src list ensures that HTTP requests aren’t made for fonts that already exist.

‘Preload’ Fonts

Often you need fonts to be available to the browser as soon as possible. Unfortunately, browsers do not have this prior knowledge. By default, a browser will only download a font when that font is used on the screen.

To do this, a browser must first anayse the CSS and HTML to ensure that the element with a specific font is visible on the page. Only then will a browser start downloading a font.

Resource hints allow you to adjust that behavior of your browser. Resource hints in the form of rel = "preload" ensure that the fonts are downloaded with high priority. A browser immediately starts downloading a resource when it encounters such a hint. This means you don’t have to wait for the CSS and HTML to be analyzed.

To preload a font with resource hints place this code as early as possible in the head of the page.

<link rel="preload" href="assets/web-fonts/DMSans-Regular.woff2" as="font" type="font/woff2" crossorigin="anonymous"/>

Be Cautious When Using preload to Load Fonts

Although preload is highly effective at making fonts discoverable early in the page load process, this comes at the cost of taking away browser resources from the loading of other resources.

In addition, using preload as a font-loading strategy should also be used carefully as it bypasses some of the browser’s built-in content negotiation strategies. For example, preload ignores unicode-range declarations, and if used prudently, should only be used to load a single font format.

The Font Loading API

The Font Loading API provides a scripting interface to define and manipulate CSS font faces, track their download progress, and override their default lazyload behavior.

For example, if you’re sure that a particular font variant is required, you can define it and tell the browser to initiate an immediate fetch of the font resource:

var font = new FontFace("Awesome Font", "url(/fonts/awesome.woff2)", {
  style: 'normal', unicodeRange: 'U+000-5FF', weight: '400'
});
 
// don't wait for the render tree, initiate an immediate fetch!
font.load().then(function() {
  // apply the font (which may re-render text and cause a page reflow)
  // after the font has finished downloading
  document.fonts.add(font);
  document.body.style.fontFamily = "Awesome Font, serif";
 
  // OR... by default the content is hidden,
  // and it's rendered after the font is available
  var content = document.getElementById("content");
  content.style.visibility = "visible";
 
  // OR... apply your own render strategy here...
});

Further, because you can check the font status (via the check()) method and track its download progress, you can also define a custom strategy for rendering text on your pages:

  • You can hold all text rendering until the font is available.
  • You can implement a custom timeout for each font.
  • You can use the fallback font to unblock rendering and inject a new style that uses the desired font after the font is available.

Best of all, you can also mix and match the above strategies for different content on the page.

For example, you can delay text rendering on some sections until the font is available, use a fallback font, and then re-render after the font download has finished.

The Font Loading API is not available in older browsers. Consider using the fontfaceobserver or the WebFontloader library to deliver similar functionality,

Proper caching is a must

Font resources are, typically, static resources that don’t see frequent updates. As a result, they are ideally suited for a long max-age expiry— ensure that you specify both a conditional ETag header, and an optimal Cache-Control policy for all font resources.

If your web application uses a service worker, serving font resources with a cache-first strategy is appropriate for most use cases.

You should not store fonts using localStorage or IndexedDB; each of those has its own set of performance issues. The browser’s HTTP cache provides the best and most robust mechanism to deliver font resources to the browser.

Preconnect to Critical Third-Party Origins

If your site loads fonts from a third-party site, it is highly recommended that you use the preconnect resource hint to establish early connection(s) with the third-party origin.

Resource hints should be placed in the head of the document. The resource hint below sets up a connection for loading the font stylesheet.

<head>
  <link rel="preconnect" href="https://fonts.com">
</head>

To preconnect the connection that is used to download the font file, add a separate preconnect resource hint that uses the crossorigin attribute. Unlike stylesheets, font files must be sent over a CORS connection.

<head>
  <link rel="preconnect" href="https://fonts.com">
  <link rel="preconnect" href="https://fonts.com" crossorigin>
</head>

When using the preconnect resource hint, keep in mind that a font provider may serve stylesheets and fonts from separate origins. For example, this is how the preconnect resource hint would be used for Google Fonts.

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

Remember, these <link> tags should be placed as early in the document as possible.

Using Self-hosted Fonts

On paper, using a self-hosted font should deliver better performance as it eliminates a third-party connection setup. If you are considering using self-hosted fonts, confirm that your site is using a Content Delivery Network (CDN) and HTTP/2.

If you use a self-hosted font, it is recommended that you also apply some of the font file optimizations that third-party font providers typically provide automatically—for example, font subsetting and WOFF2 compression.

Use Fewer Web Fonts

The fastest font to deliver is a font that isn’t requested in the first place. System fonts and variable fonts are two ways to potentially reduce the number of web fonts used on your site.

A system font is the default font used by the user interface of a user’s device. System fonts typically vary by operating system and version. Because the font is already installed, the font does not need to be downloaded. System fonts can work particularly well for body text.

To use the system font in your CSS, list system-ui as the font-family:

font-family: system-ui

On the other hand, the idea behind variable fonts is that a single variable font can be used as a replacement for multiple font files. Variable fonts work by defining a “default” font style and providing “axes” for manipulating the font.

For example, a variable font with a Weight axis could be used to implement lettering that would previously require separate fonts for light, regular, bold, and extra bold.

Not everyone will benefit from switching to variable fonts. Variable fonts contain many styles, so typically have larger file sizes than individual non-variable fonts that only contain one style. Sites that will see the largest improvement from using variable fonts are those that use (and need to use) a variety of font styles and weight.

Advanced Font Loading Techniques

Now that you know everything you need to know about font loading, we can move on to the more advanced techniques.

These techniques contain example code for you to re-use. Place this code in the head of the page and see the speed difference.

The ‘bare minimum’

With the bare minimum technique makes smart use of the available browser functions such as preloading and font display: swap. This technique is browser-based and is especially effective for loading a small number of fonts (1 to 2), hosted on your own server.

First make sure the fonts are available as soon as possible with preloading. Then, via the font display: swap, ensure that visitors does not experience the Flash Of Invisible Text.

For returning visitors (who can fetch the fonts at zero latency from their browser cache) this is the fastest technique while for new visitors it gives an acceptably short FOUT.

<link rel="preload" href="/myFont400.woff2" 
      as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/myFont700.woff2" 
      as="font" type="font/woff2" crossorigin>
 
<style>
@ font-face {
    font-family: 'myFont';
    font-weight:400;
    display:swap; /*enabled fallback font and FOUT*/
    src: local('myfont'),url('myFont400.woff2') format('woff2');
}
@ font-face {
    font-family: 'myFont';
    font-weight:700;
    display:swap; /*enabled fallback font and FOUT*/
    src: local('myfont'),url('/myFont700.woff2') format('woff2');
}
body {
    // myFont + fallback
    font-family: 'myFont',sans-serif;
    font-weight:400;
}
h1,h2,h3,h4,h5,h6{
    font-weight:700;
}
</style>

De ‘Font with a class’

With this technique fonts are loaded with JavaScript. Once all fonts are loaded a class is added to the page. That class activates your custom web font. For returning visitors a simple  session storage (or local storage) trick speeds up this process. 

<link rel="preload" href="/myFont400.woff2" 
      as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/myFont700.woff2" 
      as="font" type="font/woff2" crossorigin>
<style>
@ font-face {
    font-family: 'myFont';
    font-weight:400;
    src: local('myfont'),url('/myFont400.woff2') format('woff2');
}
@ font-face {
    font-family: 'myFont';
    font-weight:700;
    src: local('myfont'),url('/myFont700.woff2') format('woff2');
}
body {
    // fallback
    font-family: sans-serif;
}
html.fl body{
    // web font
    font-family: 'myFont';
}
</style>
<script>
(()=>{
    if( "fonts" in document ) {
        // Optimization for Repeat Views
        if( sessionStorage.fl ) {
            document.documentElement.className += " fl";
	    return;
        }
 
        // Load font
        Promise.all([
            document.fonts.load("1em myFont"),
            document.fonts.load("700 1em myFont")
        ]).then(()=>{
            document.documentElement.className += " fl";
            sessionStorage.fl = true
        });
    }
})();
</script>

The ‘2 Stage Render’ Solution

The 2 stage render solution is especially suitable for more heavy font usage that need multiple weights and versions of the same font (300,400,600,700, italic etc).

This method, which builds on the ‘Font with a class’ technique, quickly load a smaller font with only letters, numbers and punctuation. The smaller font is of course created with font synthesis. In the meantime, in parallel the full fonts are fetched by the browser and once they are loaded the are activated with a class.

This technique prevents layout shifts (when a fallback font is swapped with the final font) that could occur every time a font is loaded. Preloading a small font created a Flash Of Unstyled Text extremely early (which is much less noticeable then a later FOUT).

<link rel="preload" href="/myFontSubset400.woff2" 
      as="font" type="font/woff2" crossorigin>
<style>
@ font-face {
    font-family: 'myFontSubset';
    font-weight:400;
    src: local('myfont'),url('/myFontSubset400.woff2') format('woff2');
}
@ font-face {
    font-family: 'myFont';
    font-weight:400;
    src: local('myfont'),url('/myFont400.woff2') format('woff2');
}
@ font-face {
    font-family: 'myFont';
    font-weight:700;
    src: local('myfont'),url('/myFont700.woff2') format('woff2');
}
body {
    // fallback
    font-family: sans-serif;
}
html.flsubset body{
    // web font
    font-family: 'myFontSubset';
}
html.fl body{
    // web font
    font-family: 'myFont';
}
</style>
<script>
(()=>{
    if( "fonts" in document ) {
        // Optimization for Repeat Views
        if( sessionStorage.fl ) {
            document.documentElement.className += " fl";
	    return;
        }
 
        document.fonts.load("1em myFontSubset").then(() => {
            document.documentElement.className += " flsubset";
 
            // Load font
            Promise.all([
                document.fonts.load("1em myFont"),
                document.fonts.load("700 1em myFont")
            ]).then(()=>{
                document.documentElement.className += " fl";
                sessionStorage.fl = true
            });
        });
    }
})();
</script>

Conclusion

Web fonts still be a performance bottleneck but we have an ever-growing range of options to allow us to optimize them to reduce this bottleneck as much as possible.

Further Reading

How useful was this post?

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

We are sorry that this post was not useful for you!

Let us improve this post!

Tell us how we can improve this post?

Similar articles you may like

Sunil Pradhan

Hi there 👋 I am a front-end developer passionate about cutting-edge, semantic, pixel-perfect design. Writing helps me to understand things better.

Add comment

Stay Updated

Want to be notified when our article is published? Enter your email address below to be the first to know.

Sunil Pradhan

Hi there 👋 I am a front-end developer passionate about cutting-edge, semantic, pixel-perfect design. Writing helps me to understand things better.