Matt Hobbs (@TheRealNooshu) is Head of Frontend Development at the Government Digital Service. An experienced frontend developer, he is passionate about using his skills to build accessible and performant user interfaces. Matt makes a point of keeping on top of the latest technology and tools, is interested in all aspects of interface development, and is a keen advocate for best practices.
In this post I’m going to jot down a few thoughts to a question that has been intriguing me for a while now. “What font-display
setting should be used to improve the experience for all users?”. Before I get into that, let’s go over a few of the basics.
Typography on the web
The world wide web is possibly the greatest infrastructure mankind has ever created. Its ability to communicate and convey ideas to a global audience is unprecedented. As the saying goes on the web, “content is king” and the written word makes up a huge percentage of that content. Even non-decorative images should have words in the markup to describe them (alt
attributes). The web would be a pretty boring place if it were full of pages filled with paragraph after paragraph of similar text.
Thankfully, the web is a visual medium with many opportunities to convey ideas through the use of design and typography. For years designers were desperate to bring custom typography to the web and break out from the limitation of ‘web-safe’ fonts. First there was the ‘image replacement’ technique, then there was Scalable Inman Flash Replacement (sIFR), next came cufón. But all these techniques had flaws.
Eventually a browser native font loading technique called @font-face
started to land in browsers around 2008 (although it first appeared in the CSS2 specification as far back as 1998). This was a great addition to the web, solving many of the issues related to accessibility and maintainability in the older techniques listed above. But @font-face
came with its own set of challenges. Primarily the different font formats, and the way they are loaded.
Font formats
The web wouldn’t be the web if there weren’t competing standards between browsers. The same is true for fonts: EOT for Internet Explorer, SVG for older versions of Android, WOFF/WOFF2 for modern browsers. That’s not to mention TTF and OTF fonts which also had varying browser support. In the end the @font-face
rule was very flexible. List all the versions available and the browser will choose the first version it supports.
Font loading
A real problem came when the rendering of these fonts was considered. How do you display text on a web page using a custom font, when the font has yet to be downloaded by the browser? For many there was no consistency in how browsers would render text styled with a custom web font. There’s a whole article by Zach Leatherman – ‘A Historical look at FOUT and FOIT‘ dedicated to the subject that is well worth a read. FOIT (Flash of Invisible Text) acts much like a placeholder for the text. It worked in the fact it allowed the page to be rendered. But should a user be on a slow connection (or if the font files were large) a user could be looking at the empty shell of a webpage for many seconds. Users in this position are unable to complete the primary function for visiting the website (reading the content). This leads to a poor experience for users. FOUT (Flash of Unstyled Text) on the other hand is when text is rendered with the default system fonts before the webfont has been loaded. Developers really had no control over this process. Then in January 2016 a new @font-face
descriptor was introduced to browsers (behind an experimental flag): font-display
.
What is font-display
?
So what is the font-display
descriptor?
Before I explain, I just want to mention that font-display
has no effect on the way in which a font is downloaded from the server by the browser. No matter what this setting, the browser will request the fonts from the server and they will be sent.
But what font-display
does control is how a font is presented (or even displayed 🙂 ) to a user during the webfont download phase. By setting a specific value on the @font-face
at-rule, the browser will respond differently. It comes with 5 possible values which are explained below:
auto
This is where the browser will choose the font display strategy itself.
block
The browser will render invisible placeholder text for a short time. Depending on the browser used it can be up to 3 seconds. Beyond 3 seconds if the font hasn’t downloaded the next font in the CSS font stack is used. The browser then has an infinite amount of time in which the fallback font can be swapped out for the webfont (once downloaded).
swap
The browser will render the fallback font almost immediately. Within 100ms or less is recommended in the spec. Just like block
, the browser has an infinite amount of time in which to swap out for the webfont once it has downloaded.
fallback
The browser will render invisible placeholder text for a very short period of time (~100ms), then (if the webfont hasn’t loaded) it will render the next font in the CSS font stack. The key difference here is that the swap period is only 3 seconds. In other words, if the webfont downloads after the 3 second cut-off, text will never be rerendered. The fallback font in the CSS font stack will continue to be used.
optional
Optional is very aggressive in terms of rendering rules. The browser first displays invisible placeholder text. It is then given 100ms to render the webfont. Beyond the first 100ms the next font in CSS font stack will be used. With optional, once the fallback font has be rendered, the webfont won’t ever be rendered. There is no swap period in optional
.
So which font-display
setting is best?
So now I’ve gone over some background, this question is what I want to discuss for the rest of this post. And as with many of these types of questions, it really all depends on what you define ‘best’ as.
Fast connections
If you’re purely talking about perceived performance and connection speed isn’t an issue then I’d say font-display: swap
is best. When used, this setting renders the fallback font almost instantly. Allowing a user to start reading page content while the webfont continues to load in the background. Once loaded the font is swapped in accordingly. On a fast, stable connection this usually happens very quickly.
‘Not-fast’ connections
But it’s important to remember that not all users are on a fast and stable connections. Some are on slow and unstable connections. So what changes when we consider these users? I’d like to be upfront from the start and say I’m of the opinion that font-display: swap
isn’t best for these users. Let me go into the reason why below.
The infinite swap period
The main reason I’m of the opinion that font-display: swap
can be bad for users on slow connections is the ‘infinite swap period’. When used it’s basically saying to the browser: “no matter how long it takes for the webfont to load, swap the rendered fallback font for it when it does”.
Not all fonts are created equally
This is an issue because each fonts fundamental metrics may differ. Each font will have a slightly different baseline, x-height, median, and cap height. There’s also the difference in how letters are aligned with each other. As the tracking, leading, and kerning will differ slightly between fonts.
You also have to factor in human error when fonts are made. Glyphs within a font can sit slightly differently in the fonts ‘bounding box’ compared to other fonts.
Essentially what I’m trying to say is that when two fonts get swapped, you aren’t always going to get a 1-to-1 replacement in terms of size and position for each glyph. What could happen is the content could shift in any direction:
In the example above you see an emulated Moto G4 on The Telegraph. The font is swapping from ‘Georgia’ (its fallback font), to their own custom webfont. As you can see, because of the differences in the font metrics the whole content shifts. This process is often called Flash of Unstyled Text (FOUT). Note It is also sometimes called flash of unstyled content (FOUC).
This issue was predicted and well documented to an incredible amount of detail in the CSS Fonts Module Level 4 specification. Reading through it, the browser goes through 5-6 sets of font matching algorithms to try to minimise this problem. But even with all the automated matching involved, it’s still going to happen with some fonts at certain device widths.
The delayed content shift
Now let’s put ourselves in the shoes of a user on this Moto G4 device. Maybe they are in a rural area trying to catch up on the news. They have loaded ‘telegraph.co.uk’. News websites with lots of third party scripts can be slow at times, so let’s imagine they are using font-display: swap
. The fallback font ‘Georgia’ has rendered and they are already reading the article. swap
has now given the browser the permission to swap out the fallback font for the webfont, no matter when it loads. For a very slow connection that could be 10-20 seconds later. By this point our user is well into reading the article. The font downloads, the font is swapped, content shifts and they lose where they were in the article. That doesn’t sound like a great experience, in fact it sounds quite frustrating! Especially if it happens on most websites you visit.
Can this shift be detected?
So other than visually, can this shift be detected programmatically e.g. via Cumulative Layout Shift (CLS)? Let’s examine the Chrome ‘Performance’ panel and run a performance audit and see:
This is quite a busy screenshot so I’ll go through it here. Once the performance audit has run Chrome takes a few seconds to collate and display the results. The width of the image shows the browser tasks that happened across part of the page load. In the screenshot I’ve zoomed into two particular frames that we’re interested in. This is the frame where the browser swaps from the fallback font, to the web font (red arrows). You can also see at this point in time Chrome has highlighted in the ‘Experience’ row that a layout shift has occurred. Clicking the red shift rectangle (green arrow) brings up a full summary panel giving you detailed information about this particular shift (all described in the image above).
It’s important to remember that you can dig into a lot more detail about what is happening in the browser at this point in time (e.g. browser main thread) by investigating the other rows in the performance tab. For example: if you are seeing a huge shift at a particular point in time, you could examine exactly what JavaScript is firing and causing it. There’s a whole post dedicated to this tab ‘Get Started With Analyzing Runtime Performance’ if you are interested in knowing more.
Does this actually happen ‘in the wild’?
I know the above user scenario all sounds very contrived, so let’s see if it happens to real users. To find out we’ll pull some data together and do some testing.
The folks over at MLab regularly publish a global dashboard containing heaps of interesting data about connection speeds in different countries. If you drill down into the data it also gives you information about individual towns and villages. So let’s pick out a village from the United Kingdom (where I live): Auchencairn.
Auchencairn is a village in the historical county of Kirkcudbrightshire in the Dumfries and Galloway region of Scotland. The reported median download speed for this village is 0.28 Mbit/s, and a median upload speed of 0.28 Mbit/s (from a sample size of 26 downloads and 23 uploads). Now as you can probably tell, this isn’t a quick connection! It’s actually slightly slower than a 2G connection. Unfortunately for the village the mobile coverage is also quite spotty, with only 2 of the 4 providers only able to cover with basic data (according to Ofcom’s mobile coverage map) which offers up to 3G speeds. This observation is also confirmed by examining the nperf connection data too.
In this case our user from Auchencairn is checking the latest news. So let’s test the Telegraph on this connection speed and see what happens. By using WebPageTest and a Cloudflare Worker we can see how an article page renders under our users connection conditions while varying values of the font-display
property for the Telegraphs webfonts. For more information on how this is done, check out ‘Exploring Site Speed Optimisations With WebPageTest and Cloudflare Workers‘ by Andy Davies.
Note: I couldn’t actually get the article page to load in WebPageTest using the connection settings that our user has listed above. After 120 seconds the test timed out! I even tried forcing the test to run longer, but after many minutes I aborted. In the end I decided to increase the connection speed to 0.6 Mbit/s download, 0.6 Mbit/s upload. Once done it loaded. So the speed the tests are running at below are 114% quicker than the one our fictitious user from Auchencairn has!
Block
Above we see the filmstrip of how the article loads with font-display: block
. Note: Cropped filmstrip starts at 6 seconds.
Important timings:
- First Paint: 7.902s
- Page layout with invisible placeholder text: 8.102s
- Largest Contentful Paint (LCP): 11.002s
- Webfont swapped in: 15.103s
- Diff (first text to swap): 4.1s
So let’s examine what is happening here. Pixels are painted to the screen at 7.9 seconds into the page load. The page looks structurally complete 200ms later, but we have no text. As a user we can’t read anything yet. It’s not until 3 seconds later before text is rendered. Then finally 4.1 seconds later the webfont is swapped with the fallback font. 15.1 seconds into the page navigation the page is finally completely stable for our user.
Swap
Above we see the filmstrip of how the article loads with font-display: swap
. Note: Cropped filmstrip starts at 8 seconds.
Important timings:
- First Paint: 8.102s
- Page layout with fallback text (LCP): 8.302s
- Webfont swapped in: 12.102s
- Diff (first text to swap): 3.8s
For swap
we see the first pixels painted at 8.1 seconds. 200ms later the page structure is complete and the fallback font is rendered. The webfont is finally swapped in 3.8 seconds later (12.1 seconds after first navigation). At this point the page is stable and ready to be read.
Optional
Above we see the filmstrip of how the article loads with font-display: optional
. Note: Cropped filmstrip starts at 7 seconds.
Important timings:
- First Paint: 7.702s
- Page layout with invisible text: never
- Largest Contentful Paint: 7.802s
- Webfont swapped in: never
- Diff (first text to swap): 0s
Optional
is one of the simplest results to examine. The first pixels are rendered at 7.7 seconds. 100ms later the page structure is completed and the fallback font is rendered. That’s it! Even though the webfont is downloading in the background it will never be shown during this page’s lifecycle. If the user were to navigate to another page on the site that uses the same font, that’s when they will see the font (since it now exists in the browser cache), but for the current page the font won’t be used.
Fallback
Above we see the filmstrip of how the article loads with font-display: fallback
. Note: Cropped filmstrip starts at 8 seconds.
Important timings:
- First Paint: 8.102s
- Page layout with invisible text: 8.235s
- Largest Contentful Paint: 8.402s
- Font swapped in: never
- Diff (first text to swap): 0s
Examining the above timings, we have the first pixels painted to the screen at 8.1 seconds. Around 100ms later the page structure is completed, but with no text rendered. This state isn’t seen in the filmstrip due to the filmstrip timings used. Approximately 200ms later the fallback font is rendered. Because the webfont takes another 3+ seconds to load, the swap never happens because it is beyond the swap cutoff point. The page is now stable (from a content POV) for the user to read.
Results
Below you will see all of the results from above tests together. Listed are the render time points (in seconds) for each of the font-display
values.
Font-display | Fallback font (s) | Webfont (s) | Diff (s) |
---|---|---|---|
Block | 11 | 15.1 | 4.1 |
Swap | 8.3 | 12.1 | 3.8 |
Optional | 7.8 | n/a | 0 |
Fallback | 8.4 | n/a | 0 |
The difference between the fallback font rendering and the webfont swapping in is important. It’s worth considering this value when considering webfont usage for people with a poor connection (and / or) older devices. Now 4 seconds may not sound like much, but remember the results above are from a connection that is 114% quicker than our fictional user in Auchencairn! The true result is likely to be a lot worse on an even slower connection and device. Also, both block
and swap
are outside of the 3 second timeout that most browser vendors have adopted (as seen in the specification). So 3 seconds seems like a reasonable time to aim for as a maximum time for font swapping.
Warning: It can get so much worse!
It’s worth mentioning that results can get so much worse quickly for users on a very slow connection when Cross-Origin Resource Sharing (CORS) rears its ugly head. In setting up the tests to modify the font-display
setting I happened to point the modified CSS at the incorrect URLs for the font files. So instead of the fonts being served from the same domain as the HTML, they were now considered ‘Cross-Origin’. But the server wasn’t set up to send an Access-Control-Allow-Origin
header (either with the *
or the HTML’s domain).
These missing headers trigger the browser to send preflight requests for each of the fonts. Preflight requests are very small OPTIONS
method requests of approximately 2 KB in size. The preflights allow the server to see details of the font request before it decides if the browser should send the request for the actual fonts. In our case the browser is essentially asking the server permission to be allowed to send the actual font requests. These preflight requests can be seen in the waterfall below:
There’s a lot going on in this waterfall so let’s step through it. Requests 15-18 are the preflights sent from the browser to the server. Due to the limited and maxed out bandwidth these take 8 seconds to complete. What makes matters worse is there’s a whole TCP connection negotiation setup in there too. Only when the browser has received permission back from the server will it send the actual font requests. These then take another 2-3 seconds to completely download. During this time the browser is almost idle, waiting for something to do.
This is understandable, but what I didn’t expect is the impact this has on font rendering. If we again refer to the CSS Fonts specification for font-display: fallback
:
Gives the font face an extremely small block period (100ms or less is recommended in most cases) and a short swap period (3s is recommended in most cases).
So I’d expect a short 100ms delay before the fallback font is rendered and the user can start reading the content. But in reality this doesn’t happen (in Chrome at least) as you will see below:
Note: Compressed filmstrip starts at 7.5 seconds.
Important timings:
- First Paint: 7.802s
- Page layout with invisible text: 7.902s
- Largest Contentful Paint: 15.903s
- Font swapped out: never
Here we have 8 seconds between the first pixels being rendered to the first text actually showing up on the page. That’s a pretty huge chunk of time for a user to wait. If you compare the CORS broken vs CORS fixed filmstrips the difference is quite obvious:
Note: Cropped filmstrip starts at 7.5 seconds.
Interestingly this only happens with font-display: fallback
and font-display: block
. font-display: swap
and font-display: optional
under the same conditions render the fallback font at ~8 seconds. I believe this is because both these settings have a ‘block’ period in their loading timeline, and the specification says:
The first period is the font block period. During this period, if the font face is not loaded, any element attempting to use it must instead render with an invisible fallback font face.
But I may be completely wrong. If anyone has any thoughts or information on this please do let me know!
So the key takeaway from all this is make sure you set the correct Access-Control-Allow-Origin
headers if you are serving fonts ‘Cross-Origin’. If you don’t the CORS preflights will essentially be adding a large TTFB (Time to First Byte) onto your font requests.
What can be done to minimise the shift?
So what can you do to minimise the shifts seen in these extreme cases? Well, the obvious solution (that designers will hate) is ask yourself if you need the webfont at all? Is there an alternative system font that is suitable, that matches closely with the design? Not going to fly with the design team? Okay then, read on:
Font matching
You could consider having a play around with Font style matcher. A tool designed to help you match a webfont’s x-heights and widths with the fallback font. With this tool it should be easier to find a set of closely matching fonts. So consider changing the CSS font stack if your current fallback and webfont are vastly different in terms of style and metrics. This is even advised in the specification:
Authors are advised to use fallback fonts in their font lists that closely match the metrics of the downloadable fonts to avoid large page reflows where possible.
In the future there should be a whole new set of font metric settings to play with in the @font-face
rule (ascent-override
, descent-override
and line-gap-override
). These now exist in the CSS Fonts Module Level 4 specification, but from what I can find no browser supports them yet.
Font load optimisation
Font matching may only get you so far depending on the fonts used. Other tactics you could employ are to reduce the font size so it downloads quicker, and optimise how your fonts are being served. I won’t go into the details here as there have been many (many!) blog posts written all about these subjects. But here are a few links to get you started:
- A Comprehensive Guide to Font Loading Strategies – Zach Leatherman
- Should you self-host Google Fonts? – Barry Pollard
- The Fastest Google Fonts – Harry Roberts
- Preloading Fonts and the Puzzle of Priorities – Andy Davies
- Minimising font downloads – Jake Archibald
- 3 Tips for Faster Font Loading – Jeremy Wagner
- The Five Whys of Web Font Loading Performance | performance.now() 2018 – Zach Leatherman
Summary
So in this blog post we’ve covered a lot! From some of the basics of typography, through font formats all the way to font shifting, the font-display
setting and its impact it can have on users with very slow connections.
If there’s one thing I’d like readers to take away from this post it’s that font-display: swap
is a very good option for users with a fast internet connection. But its infinite swap period could be frustrating for users on very slow and unstable connections. If you have users viewing your site under these conditions (I’m pretty certain you will at some point in time), then it may be worth considering font-display: fallback
or even font-display: optional
. Both have a short swap period (or no swap period), meaning once the fallback font is rendered and the 3 second timeout is exceeded, the font won’t change for the rest of the page lifecycle.
The CSS Fonts Level 4 actually points to this use case in the specifications for fallback
:
This value should be used for body text, or any other text where the use of the chosen font is useful and desired, but it’s acceptable for the user to see the text in a fallback font. This value is appropriate to use for large pieces of text.
Finally, I’ve been involved in improving the web performance of GOV.UK for a number of years now. This work has included improving the font loading strategy, in which we currently use font-display: fallback
. We have data to suggest many users are on fast connections (4G+), but there’s also data to show that we have users on very slow connections. So I’d personally prefer to take a hit of 100ms on our LCP, where an invisible font is rendered before showing our fallback font (i.e. font-display: fallback
), than potentially causing users on very slow connections a delayed font shift (i.e. font-display: swap
). So you could consider this post a decision record as to why we use fallback
over swap
.
Thanks for reading, I hope you found this interesting.
Many thanks to Barry Pollard, Paul Calvano,Ben Schwarz, and Oliver Byford for their technical proof reading of this article.