Font loading strategy for single page applications
Web fonts bring a sense of identity to our projects and have become a crucial asset of product design nowadays, but they can delay content displaying in web applications, specially for slow connections.
With no effective font loading strategy, users will experiment what's call FOIT (Flash of Invisible Text) as the font files are downloading.
Instead it's preferable to go for FOUT (Flash of Unstyled Text), users will see content sooner with a font from the system and switch to the web font later.
A while ago I wrote about how to properly load a web font in static sites with a recipe which included a deferred font bundle, font observation to switch when fonts are usable, and a combination of stylesheet injection with web storage for future visits.
As a follow up, this article will explain that strategy adapted to web applications architecture and stack.
Providing a fallback font
Like any lazy loading strategy for fonts, the first thing we need to do is to show a fallback family while we wait for web fonts to be usable.
In the CSS of your project add a default system font and another rule with a class to switch to the web one.
/* system fonts */
body {
font-family: Georgia, serif;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: Arial, serif;
}
/* web fonts */
.lato-ready body {
font-family: 'Lato', sans-serif;
}
.roboto-ready h1,
.roboto-ready h2,
.roboto-ready h3,
.roboto-ready h4,
.roboto-ready h5,
.roboto-ready h6 {
font-family: 'Roboto', serif;
}
How this is implemented will depend in your stack, but basically these rules should be declared globally to affect all your application.
Generating a font bundle
webpack is one of the most popular tools to bundle web applications and it comes with a lot of useful features out of the box, like dynamic imports.
Every time you load a file using the import()
method, webpack will generate a new bundle and asynchronously load it for you in the browser.
In React applications, we could do this by adding the function call in the componentDidMount callback of your main component.
import React from 'react';
import Title from '../Title';
import Content from '../Content';
class App extends React.Component {
componentDidMount() {
// import font bundle
import('./font.js');
}
render() {
return (
<div className="App">
<Title>
Loading fonts on single page applications
</Title>
<Content/>
<div>
);
}
}
The good thing about this is we not only avoid blocking the content with an unloaded font family, but we also don't increase our bundle size or affect loading times, independently from how complex our font strategy is.
For further reading you can check out the standard import definition in Google's developer site and webpack documentation about its use.
The font bundle
Inside our font.js file we need to import the dependencies needed, observe the fonts and toggle the class when they are ready.
We are going to use Bram Stein's fontfaceobserver package to watch the different font stacks and store-css to load the font stylesheet.
import Observer from 'fontfaceobserver';
import { css } from 'store-css';
// import fonts stylesheet
css({
url: '//fonts.googleapis.com/css?family=Lato|Roboto:700',
crossOrigin: 'anonymous'
});
// observe body font
const bodyFont = new Observer('Lato', {
weight: 400
});
bodyFont.load().then(() => {
document.documentElement.classList.add('lato-ready');
});
// observe heading font
const headingFont = new Observer('Roboto', {
weight: 700
});
headingFont.load().then(() => {
document.documentElement.classList.add('roboto-ready');
});
If you are self-hosting your font files and your application doesn't refresh on navigations, instead of using store-css add an import
with the root of the stylesheet containing the font face declarations and use webpack's css loader to automatically include it in your bundle.
import Observer from 'fontfaceobserver';
// import fonts stylesheet
import('./fonts.css');
// observe body font
const customFont = new Observer('Your Custom Font');
customFont.load().then(() => {
document.documentElement.classList.add('custom-font-ready');
});
You can check out this solution working on this repository.
Web storage and reloads
As I described it in my article about font strategies for static sites, it is possible to combine this approach with web storage to host and detect font declarations on future navigations in case refresh happens.
For store-css is as easy as adding a storage
option.
import { css } from 'store-css';
css({
url: '//fonts.googleapis.com/css?family=Lato|Roboto:700',
storage: 'session',
crossOrigin: 'anonymous'
});
The approach and code would be identical but if your application has routing incorporated, then keeping this persistence on the font loading state won't be necessary.
Profiling and results
Testing this approach in a simple React application, throttling the network to a Fast 3G connection and the CPU down to 6x slower than my laptop in Chrome devtools, the results were:
- Without a font loading strategy, 3250ms average to display meaningful text content. This means the application initial content was ready before but the user needed to wait for the font some extra time.
- With a font loading strategy, 2300ms average, which is basically what it takes for the bundle to be downloaded and parsed, and the application to render the first screen since the fallback font is already available.
That's approximately an improvement of 30% in delivering content to the user, and server side rendering could even make this difference bigger.
If you want to read more about the negative impact of not having a font loading strategy, I suggest Monica Dinculescu's article describing her experience on a 2G connection and Zach Leatherman's metrics from profiling font displaying approaches of presidential sites back in 2016.
Wrap-up
Applying a font strategy is really easy in single page applications since we don't have to care about reloads.
We can use webpack's dynamic imports to create a separate bundle and isolate the logic there so we don't affect bundle size and loading times for our web application.
No excuses folks! Let's take advantage of the tools we have, and are probably already using, and make the web better and faster for our users.
Thanks to Even Stensberg for reviewing this article.
Do you want me to write for your publication? Click here to contact me via email.