Written by Ramón Saquete
The more sources we have on a website, the worse the WPO metrics are and the more difficult it is to optimize it for good performance. If we want to have a good balance between performance and design, we should always pay special attention to font optimization and avoid overuse.
Below are several basic optimization techniques that should always be applied. We will also explain advanced optimization techniques that are not always advisable to apply, so you should measure whether their effect is positive or negative in each situation, especially when adding additional JavaScript code.
Basic font optimization techniques
Use the minimum number of sources possible
We must try to homogenize the design using the minimum number of sources possible, being the ideal for zero yield.
If we are concerned about the performance of our site, above the design or brand image, the ideal is to optimize the downloading and painting, using directly the font of the user’s operating system. In this way we avoid the browser having to download the source. To implement this, we simply set the “font-family” property to “serif”, “sans-serif” or “monospace” in the CSS rules, so that any browser will assign the default system font according to the chosen style. If we want to take the nicest possible system font, to ensure good readability, we can set up a font stack like this:
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
The browser will display the first available font in the operating system of this stack. This list may become outdated as operating systems change, but it is not too much of a problem because the most general source will always be used, which should be at the bottom of the stack.
It is advisable to avoid loading fonts when their use is minimal and also for those cases in which if we do not specify the font for bold or italics, and in the CSS we tell it that we want to see it in any of these formats, the browser itself converts them reasonably well.
References to fonts or variants of fonts that are not used should also be removed from the CSS . Variants can be: italic, bold, bold normal, italic and bold normal, etc. If the source is not used or is not visible, it will not be downloaded, but it is preferable to shorten the CSS code as much as possible.
Caching fonts in the browser
Don’t forget to cache the fonts in the browser with the cache-control header.
Remove unused subsets of glyphs
Glyphs that do not belong to the alphabet used by the web should be removed from the fonts. For example, in the case of a web site in English and Spanish, the glyphs of the Latin alphabet are sufficient, so we do not need Chinese, Japanese, Russian or Arabic characters.
To do this we will modify the source with asubsetter. With this tool we select the subsets of glyphs we want to use. As an example, here is an online https://everythingfonts.com/subsetter and a command line https://github.com/fonttools/fonttools.
Once the set of characters that are not going to be used has been eliminated and as a prevention, it is convenient to specify to the browser, in the declaration of the font, that it should not download or apply that font to characters outside the subset chosen with the property“unicode-range“.
Below is an example of the characters that may appear in Spanish:
@font-face { font-family: 'fuente'; src: url('fuente.woff2') format('woff2'); unicode-range: U+0000-00FF; }
You can see the list of Unicode ranges here:
https://en.wikipedia.org/wiki/Unicode_block
If we are loading the font from an external Google fonts link, we can specify the chosen subset in the Google URL parameters by adding the following QueryString:
subset=cyrillic
However, Google does not always have the exact subset we ask for available and may return a larger font than necessary, so I recommend always downloading and optimizing it manually.
The subset of Latin characters is always loaded by default, so it is not necessary to specify it with subset=latin.
Use WOFF2 format
Of the five existing font formats (WOFF2, WOFF, EOT, TTF and SVG), the WOFF2 format , developed by Mozilla, is the most recommended, as it is currently compatible with all modern browsers and does not require additional compression. However, to support older browsers, the font can be added in the other formats.
EOT, TTF and SVG formats are always best sent compressed from the server (preferably with Brotli q11). The WOFF format only needs compression sometimes, depending on the case and when the optimal compression options have not been used to generate the file.
To generate a font in different formats we can use online tools such as Font Squirrel or www.font-converter.net.
Sets the correct loading order of the different source formats
When we want to support older browsers, care must be taken with the order in which fonts are added in the CSSsince the browser will use the first compatible file it finds, even if it does not have the best compression. Therefore, the correct order would be WOFF2, WOFF, EOT, TTF and SVG. EOT must come before TTF, since Internet Explorer also supports TTF but only partially:
@font-face { font-family: 'fuente'; src: url('fuente.woff2') format('woff2'), /* todos los navegadores modernos */ url('fuente.woff') format('woff'), /* navegadores que no se han actualizado */ url('fuente.eot'), /* IE9 */ url('fuente.eot?#iefix') format('embedded-opentype'), /* IE9 */ url('fuente.ttf') format('truetype'), /* Safari, Android, iOS */ url('webfont.svg#svgFontName') format('svg'); /* Safari version <= 4.1 */ }
If we do not want to support older browsers, only the WOFF2 format is sufficient. After all, if the custom font is not loaded, the system font will be loaded.
System source display before loading content
This avoids the so-called FOIT (Flash of Invisible Text) effect, which means that during page loading, the user cannot see the texts on the website until the font has finished loading. To avoid FOIT, we must use the following CSS statement within the @font-face rule:
font-display:swap;
This way the browser will display the system font, while loading the web font, changing the FOIT effect to a FOUT (Flash of Unstyled Text) effect, which consists of the user seeing the font style change all at once. This effect is also not desirable but is preferable to the user not being able to see anything.
Very few browsers currently ignore the font-display:swap statement, but the alternative is to use additional JavaScript to bypass FOIT, whereby the overhead involved in executing this JavaScript may decrease performance.
Anyway, in the last point I explain this technique and I recommend to apply it only if tests are performed, to see in each specific case if the performance improves or worsens.
If we are loading the font from a Google fonts URL, we can apply this CSS declaration by adding the following parameter to the QueryString of the URL:
display=swap
Let’s see an example of a Google Fonts URL, passing the parameters to avoid the FOIT effect and selecting the subset of Latin characters:
https://fonts.googleapis.com/css?family=Cabin:400,700&display=swap&subset=latin
Advanced font optimization techniques
Advance loading of fonts used in above the fold (but not too much)
We can minimize the FOUT and FOIT effect (in browsers that do not allow the use of “font-display:swap”), bringing forward the loading of critical fonts, i.e. those that are displayed as soon as the page loads. To do this, critical fonts can be included within a <style> tag or by using a link tag with the rel=”preload” and as=”font” properties within the HTML. Examples:
<style> @font-face { font-family: 'fuente'; src: url('fuente.woff2') format('woff2'); } /* Aquí debe ir el resto del CSS crítico para que esta técnica de resultado */</style>
Or we put the above code in an external CSS and preload the source from the HTML like this:
<link rel="preload" href="/fonts/fuentes.woff2" as="font">
This way the dependency tree only has to make one jump to get to the source (HTML → source), instead of two (HTML → CSS → source). However, the label <link> is better if we are sure that the font is always going to be used in the above the fold, since including the CSS with the <style>The download does not occur until the browser encounters some CSS rule that applies to the HTML and uses that font.
Within the critical resources, some are more critical than others, and frontloading the font to minimize FOUT is not as important as loading the above the fold styles first. So, overtaking the source too much by including it as a DATA URI (so that the HTML and the source are downloaded at the same time), is considered an anti-pattern, since the source would no longer be cacheable and would delay the loading of the rest of the critical resources. It is also usually not a good idea to preempt your upload with HTTP/2 server push.
Delay loading of fonts that are not displayed in above the fold
Fonts do not start downloading until the browser detects that there is a CSS rule that uses that font. But if they are fonts that are only used below the fold (for example, an exclusive font for the footer), they will be downloaded and may interfere with the download of critical resources, so I recommend to start downloading them in a deferred way. For that they can be loaded from a CSS containing all the non-critical styles whose delayed loading can be implemented like this:
<noscript id="deferred-styles"> <link rel="stylesheet" type="text/css" href="/bundle-css-no-critico-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 loading the entire page (due to the defer attribute) and once the browser is ready to perform a screen refresh:
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); }
Avoiding FOIT and multiple repaints with JavaScript
Each time a font is loaded, the browser performs a repaint with the new font . If there are several fonts, this repainting may occur several times. If this is our case, I recommend using the FontFaceOBserver JavaScript library that you can download here and that uses the JavaScript Font Loading API in those browsers that support it.
This library allows us to implement multiple font loading strategies, but most importantly, it allows us to force and observe the loading of existing fonts in the CSS and associate code when a set of fonts have finished loading.
This way we can load the fonts without the need for an associated CSS rule and, once loaded, change a class in the HTML that makes them all show at once.
Example:
CSS code:
@font-face { font-family: 'Mi fuente'; src: url(mi-fuente.woff2) format('woff2'), url(mi-fuente.woff) format('woff'); unicode-range: U+0000-00FF; /* podemos especificar font-display:swap; para que las herramientas de WPO de Google nos puntuen mejor, pero no servirá de nada ya que lo vamos a implementar con JavaScript */} @font-face { font-family: 'Mi fuente'; src: url(mi-fuente-italica.woff2) format('woff2'), url(mi-fuente-italica.woff) format('woff'); font-style: italic; unicode-range: U+0000-00FF; } /* Fuente del sistema que se usará mientras se carga la página */html{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } /* Fuente que se usa cuando ya está cargada la fuente */html.fonts-loaded{ font-family: 'Mi fuente'; } html.fonts-loaded em{ font-style: italic; }
JavaScript code:
var normal = new FontFaceObserver('Mi fuente'); var italic = new FontFaceObserver('Mi fuente', { style: 'italic' }); var html = document.documentElement; //console.log('cargando fuentes'); if (sessionStorage.fontsLoaded) { document.documentElement.classList.add('fonts-loaded') } else { Promise.all([ normal.load(), italic.load() ]).then(function () { //console.log('fuentes cargadas'); html.classList.add('fonts-loaded'); sessionStorage.fontsLoaded = true; }).catch(function () { //console.log('fallo cargando fuentes'); sessionStorage.fontsLoaded = false; }); }
In the example we save a variable in the sessionStorage object that serves as a hint, to know if the sources are most likely to be in the cache and thus load them directly, without the need to download them again.
Conclusion
The more fonts we have, the less efficient the loading and the more complex the optimization. Not using fonts or using at most one font is the best strategy to save development costs and improve performance, at the cost of sacrificing something in the design that sometimes may not be very relevant.