Web font optimisation

Ramón Saquete

Written by Ramón Saquete

The more fonts we have on our website, the worse our WPO metrics get, and it becomes increasingly more difficult to optimise it to have a good performance. If we strive to achieve a good balance between performance and design, we must always take special care of web font optimisation and avoid an abusive use of this resource.

In this post we are going to suggest various basic optimisation techniques that should always be put in force. We will also dive into advanced optimisation techniques, the application of which isn’t always recommended. We must measure their effect to see whether it’s positive or negative in each situation, especially when it comes to adding additional JavaScript code.

Basic font optimisation techniques

Keep the amount of fonts used to a minimum

We should try to homogenise the design using as few fonts as possible. The ideal amount of fonts for optimal performance is zero.

When optimising fonts, there’s nothing as fast as not loading any fonts at all 🚀

If we’re worried about our site’s performance, more so than its design or our brand image, the best thing we can do to optimise its download and painting is to use directly the user’s system font. This way, we keep the browser from having to download any font. In order to implement this, we only need to give the “font-family” property the “serif”, “sans-serif” or “monospace” values in our CSS rules, and the browser will assign the default system font according to the chosen style. If we want to choose the most appeasing system font, in order to ensure a good readability, we can define a pile of fonts, as shown below:

font-family: -apple-system, 
                BlinkMacSystemFont, 
                "Segoe UI", 
                Roboto, 
                Oxygen-Sans, 
                Ubuntu, 
                Cantarell, 
                "Helvetica Neue", 
                sans-serif;

The browser will display the first system font available from this pile. The list can become outdated, though, as operating systems change, but that’s not too big of an issue, because the most typical font will always be used, which should be at the end of this pile.

It’s recommended to avoid loading fonts when their use is reduced to a minimum, as well as in cases where we don’t specify a font for bold and italic styles, but in the CSS we do tell the browser that we want to see it in any of those styles. The browser itself converts them reasonably well.

We must also remove from the CSS any references to fonts or their versions, which we aren’t using. These versions can be: italics, strong bold, regular bold, italics and regular bold, etc. If the font is not used or is invisible, it won’t be downloaded, but it’s preferable to abbreviate the CSS as much as possible.

Caching fonts in the browser

Don’t forget to cache fonts in the browser using the cache-control header.

Remove unused glyph subsets

It’s important to remove from fonts all glyphs not belonging to the alphabet used on the website. For example, if we have a website in English and Spanish, and Latin alphabet glyphs are enough for us, we don’t need Chinese, Japanese, Russian or Arabic characters.

To do this, we will modify the font using a “subsetter“: With this tool, we will select glyph subsets we want to use. For example, here’s an online guide https://everythingfonts.com/subsetter and a command-line one https://github.com/fonttools/fonttools. Once we’ve removed the set of characters that aren’t going to be used, as a preventive measure, we should specify to the browser in the font declaration that this font shouldn’t be downloaded or applied to characters not belonging to the chosen subset, with the “unicode-range” property.

Below, you can see an example for characters, which may appear in Spanish:

@font-face {
  font-family: 'font';
  src: url('fuente.woff2') format('woff2');
  unicode-range: U+0000-00FF; 
}

The list of Unicode ranges can be found here: https://en.wikipedia.org/wiki/Unicode_block

If we’re loading the font from an external Google Fonts link, we can specify the chosen subset in Google’s URL parameters, by adding the following QueryString:

subset=cyrillic

However, Google won’t always have the exact subset we request available, and it’s possible it may return a bigger font than we need. For that reason, I recommend always downloading and optimising it manually.

The Latin character subset is always loaded by default, so we don’t need to specify it with subset=latin.

Using the WOFF2 format

Out of the five existing font formats (WOFF2, WOFF, EOT, TTF and SVG), the WOFF2 format, developed by Mozilla, is the most recommended one, as it is currently compatible across all modern browsers and it doesn’t need additional compression. Nevertheless, we can add a font to support older browsers in the remaining formats.

It is always best that EOT, TTF and SVG are sent compressed from the server (preferably with Brotli q11). The WOFF format only needs to be compressed occasionally, depending on each individual case, and when the file wasn’t generated using the optimal compression options.

To generate a font in different formats, we can use online tools such as Font Squirrel or www.font-converter.net.

Set the correct download order for the various font formats

When we wish to support older Internet browsers, we must be mindful of the order in which fonts are added to the CSS, as the browser will employ the first compatible file it finds, regardless of whether it has the best compression or not. For that reason, the correct order would be WOFF2, WOFF, EOT, TTF and SVG. EOT should go before TTF, as Internet Explorer is also compatible with TTF, but only partially:

@font-face {
       font-family: 'font';
       src: url('font.woff2') format('woff2'), /* all modern browsers */ 
       url('font.woff') format('woff'), /* not updated browsers */
       url('font.eot'), /* IE9 */
       url('font.eot?#iefix') format('embedded-opentype'), /* IE9 */
       url('font.ttf')  format('truetype'), /* Safari, Android, iOS */
       url('webfont.svg#svgFontName') format('svg'); /* Safari version <= 4.1  */
}

If we do not wish to support older browsers, WOFF2 will be enough. In the end, if the custom font doesn’t load, the system one will.

Viewing the system font before loading the content

We want to avoid the FOIT (Flash of Invisible Text) effect, which consists in users not being able to see the text on the website until the font has finished loading. To prevent this from happening, we must use inside the @font-face rule the following CSS declaration:

font-display:swap;

This way, the browser will display the system font while the website is loading, changing the FOIT effect to FOUT (Flash of Unstyled Text), which consists in users seeing how the font style suddenly changes before their eyes. While this isn’t ideal, it’s preferable to our users not seeing anything at all.

foit fout
FOIT and FOUT effects

In the present day, very few browsers pay attention to the font-display:swap declaration, but the alternative is to use additional JavaScript to avoid FOIT. The overload that comes with running this JavaScript code can worsen performance.

In any case, in the last section of this post I will explain this technique, and I recommend applying it only if the required tests are carried out, in order to see in each particular instance whether it improves or worsens performance.

If we’re loading a font from a Google Fonts URL, we can apply this CSS declaration, adding the following parameter to the URL’s QueryString:

display=swap

Let’s see an example of a Google Fonts URL, applying the parameters to prevent the FOIT effect and to select a subset of Latin characters:

https://fonts.googleapis.com/css?family=Cabin:400,700&display=swap&subset=latin

Advanced font optimisation techniques

Give priority to loading fonts used inside the above-the-fold (but not too much)

We can minimise the effects of FOIT and FOUT (in browsers, which do not allow the use of “font-display:swap”), by giving priority to loading critical fonts, i.e. those, which are displayed as soon as the page loads. To do this, we can include critical fonts inside the <style> tag, or use a link tag with the rel=”preload” and as=”font” properties in the HTML. For example:

 

 

 

 

Or, we place the above code inside an external CSS file and we preload the font from the HTML, just so:

<link rel="preload" href="/fonts/fonts.woff2" as="font">

This way, the dependency tree only has to jump once to reach the font (HTML > font), instead of two times (HTML > CSS > font). However, the <link> tag is better if we’re certain that the font is always going to be used inside the above-the-fold section, because if we include the CSS code with a <style> tag, the download won’t take place until the browser finds a CSS rule that applies to the HTML and uses said font.

Within the critical resources, some are more critical than others, and prioritising the font download to minimise FOUT is not as important as loading the above-the-fold styles first. So, giving too much priority to the font, by including it as a DATA URI (which means the HTML and the font download at the same time) is considered to be an anti pattern, because the font would stop being cacheable, and it would delay the download of other critical resources. It is neither a good idea pushing ahead its download with HTTP/2 server push.

Delay loading of fonts not shown inside the above-the-fold

Fonts do not start loading until the browser detects there is a CSS rule using said font. But if these are fonts only used below-the-fold (for example, a font used exclusively in the footer), they will be downloaded, and they could possibly interfere with the download of critical resources. I recommend deferring their download. They could be loaded from a CSS file containing all non-critical styles, the delayed download of which can the implemented as follows:

<noscript id="deferred-styles">
      <link rel="stylesheet" type="text/css" href="/bundle-non-critical-css-v452372.css"/>
    </noscript<
    <script defer src="bundle-js-v23423.js"></script>

Inside the JavaScript we will have the following lines, which will be executed after the page has loaded entirely (due to the “defer” attribute), and once the browser is ready to repaint the screen:

      var loadDeferredStyles = function() {
        var addStylesNode = document.getElementById("deferred-styles");
        var replacement = document.createElement("div");
        replacement.innerHTML = addStylesNode.textContent;
        document.body.appendChild(replacement)
        addStylesNode.parentElement.removeChild(addStylesNode);
      };
      var raf = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
          window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
      if (raf){ 
         raf(function() { window.setTimeout(loadDeferredStyles, 0); });
      }
      else{ 
         window.addEventListener('load', loadDeferredStyles);
      }

Avoid the FOIT and multiple repaints with JavaScript

TypefaceEach time a font is loaded, the browser carries out a repaint with the new font. If there are more than one, this repaint can happen several times. If this is our case, I recommend using the JavaScript FontFaceObserver library, which you can download here. It uses the JavaScript Font Loading API underneath, in browsers which support it.

This library allows us to implement multiple strategies when loading fonts, but its most important feature is that it allows us to force and observe the download of fonts present in the CSS, and to associate code to the moment when a set of fonts has finished loading.

This way, we can load fonts, without there being an associated CSS rule, and once they’ve loaded, modify a class in the HTML that will display them all at the same time.

Example:

CSS code:

@font-face {
  font-family: 'My font';
  src: url(mi-fuente.woff2) format('woff2'),
       url(mi-fuente.woff) format('woff');
  unicode-range: U+0000-00FF;
  /* we can specify font-display:swap; so that Google's WPO tools rank us better, but it won't result in anything, as we are going to implement it using JavaScript */}
@font-face {
  font-family: 'My font';
  src: url(mi-fuente-italica.woff2) format('woff2'),
       url(mi-fuente-italica.woff) format('woff');
  font-style: italic;
  unicode-range: U+0000-00FF;
}

/* System font used while the page loads */html{
   font-family: -apple-system, 
                BlinkMacSystemFont, 
                "Segoe UI", 
                Roboto, 
                Oxygen-Sans, 
                Ubuntu, 
                Cantarell, 
                "Helvetica Neue", 
                sans-serif;
}

/* Font used when it's already finished loading */html.fonts-loaded{
   font-family: 'My font';
}

html.fonts-loaded em{
   font-style: italic;
}

JavaScript code:

var normal = new FontFaceObserver('My font');
var italic = new FontFaceObserver('My font', {
  style: 'italic'
});
var html = document.documentElement;

//console.log('loading font');
if (sessionStorage.fontsLoaded) {
    document.documentElement.classList.add('fonts-loaded')
} 
else {
  Promise.all([
  normal.load(),
  italic.load()
  ]).then(function () {
    //console.log('fonts loaded');
    html.classList.add('fonts-loaded');
    sessionStorage.fontsLoaded = true;
  }).catch(function () {
    //console.log('error loading fonts');
    sessionStorage.fontsLoaded = false;
  });
}

In this example we store a variable within the sessionStorage object, which serves as a clue for us to know if fonts are more likely to be in the cache and to load them directly, without having to download them again.

Conclusion

The more fonts we have, the less efficient is our download, which makes optimisation more tricky. Not using any fonts, or using one at most is the best strategy to save money on improving performance, by sacrificing some design aspects, which at times can be irrelevant.

Ramón Saquete
Autor: Ramón Saquete
Web developer at Human Level Communications online marketing agency. He's an expert in WPO, PHP development and MySQL databases.

Leave a comment

Your email address will not be published. Required fields are marked *