6

The importance of `@font-face` source order when used with preload

 3 years ago
source link: https://nooshu.github.io/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
The importance of `@font-face` source order when used with preload

Skip navigation

The other day I decided to run a quick WebPageTest (WPT) run over the latest version of the White House website launched for President Biden’s term in office (it’s amazing what I do for fun in lockdown huh!). The WPT run returned something curious:

The above waterfall was captured using Chrome on a real Moto G4 with a 3G Fast connection. That’s a lot of font files being downloaded (red requests), 14 files in total.

Note: This blog post isn’t a criticism of the excellent work the team at the U.S. Digital Service does. It’s just an interesting observation I’ve never encountered or considered before. Being a Civil Servant myself, I understand how hard everyone works to deliver user friendly services (especially under current conditions!). Keep being awesome!

Note 2: Turns out USDS didn’t build this website!

So let’s investigate what is happening here. First thing to notice is that there are 6 fonts that are loading right after the HTML has downloaded and parsed (request 2-7). This is a sure sign that these fonts are being preloaded. What usually happens is the DOM is constructed, then the CSS downloaded to create the CSSOM. These are both combined to form the render tree. At this point, fonts referenced in the @font-face rule will be discovered and requested by the browser (assuming they are needed to render the specified text on the page).

Font load priority#

There’s an important point to consider here. The order in which you list your font sources in the @font-face rule is very important. According to the CSS Fonts Module Level 3 specification:

It is required for the @font-face rule to be valid. Its value is a prioritized, comma-separated list of external references or locally-installed font face names. When a font is needed the user agent iterates over the set of references listed, using the first one it can successfully activate. Fonts containing invalid data or local font faces that are not found are ignored and the user agent loads the next font in the list.

Or in other words, the browser will start at the top of the list looking for a font format it supports. The first one it encounters is loaded, and if successful the sources listed later are ignored. Or in code it looks like this:

@font-face {
  font-family: bodytext;
  src: url(our-test-font.woff2) format("woff2"), /* WOFF2 tried first */
       url(our-test-font.woff) format("woff"), /* WOFF tried second */
       url(our-test-font.ttf) format("opentype"); /* TTF tried if others not supported. */
}

There’s a large crossover between browsers that support WOFF and those that support WOFF2. Or to phrase it another way, if a browser supports WOFF2, it also supports WOFF. This is important, as if you are listing your WOFF fonts first in the @font-face rule, the browser is going to download and use these instead of the WOFF2 versions (which offer much better compression because they use Brotli). So for example if you do this:

@font-face {
  font-family: bodytext;
  src: url(our-test-font.woff) format("woff"), /* WOFF tried first */
  		url(our-test-font.woff2) format("woff2"), /* If WOFF is successful, WOFF2 is ignored */
       url(our-test-font.ttf) format("opentype"); /* TTF tried if other not supported. */
}

Assuming the WOFF font doesn’t 404, the WOFF2 font will never be requested by the browser!

The Waterfall#

The above information can now be used to explain what is happening with the waterfall we see above.

Preload (requests 2-7)#

Examining the code from the White House website we can see the following in the <head>:

<link rel="preload" href="...fonts/Decimal-Book.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="...fonts/Decimal-Semibold.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="...fonts/Decimal-Medium_Web.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="...fonts/MercuryTextG2-Roman-Pro_Web.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="...fonts/MercuryTextG2-Semibold-Pro_Web.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="...fonts/MercurySSm-Medium-Pro_Web.woff2" as="font" type="font/woff2" crossorigin="anonymous">

These preload links tell the browser to immediately make the requests for the WOFF2 versions of the fonts as soon as the browser parses the <head>, which it happily does as it trusts that these fonts will be used later in the pages lifecycle. Once these requests have been made and are sent over the network the browser is going to get a response back (assuming the fonts are available and don’t 404). Even if the browser were to cancel the requests, it will still receive bytes back from the server for the fonts. They just wouldn’t be used. There’s an example waterfall of this happening here and a blog post all about it here.

CSS (request 9)#

At request 9 we can see the CSS file being downloaded and parsed by the browser. Also notice how only 2 of the preloaded fonts have been fully downloaded. This is a critical part of the page load with limited bandwidth available. Under the hood the browser still needs fonts to be able to render text on screen. A preload will prime the browser cache, but it won’t add a font to the FontFaceSet. For that you either need to use the CSS Font Loading API, or the traditional @font-face rule.

Within this CSS file on request 9 we see the @font-face rules (I’ve only referenced 1 as an example, but there are many):

@font-face {
    font-family: MercuryTextG2-Semibold-Pro_Web;
    src: url(.../fonts/MercuryTextG2-Semibold-Pro_Web.woff) format("woff"),
    	url(.../fonts/MercuryTextG2-Semibold-Pro_Web.woff2) format("woff2");
    font-weight: 400;
    font-style: normal;
    font-display: block
}

Notice the ordering of the src fonts in this @font-face rule. The WOFF version of the font is listed first, then WOFF2 listed second.

The result#

The browser is now making 8 more font requests to WOFF font files (requests 10-17). To the browser it sees these as completely separate files. It doesn’t know that the glyphs contained within them are actually identical to the ones from the preloaded fonts! The browser is now doubling up on the number of fonts requested because it has stopped at the WOFF font src in the @font-face rule. It has no knowledge about the WOFF2 version directly below because it has found the first one it supports, then stopped (as defined in the specifications).

This is probably easier to explain on a waterfall chart:

The above waterfall is from Chrome on a Moto G4 with a 3G Fast connection, but I have also managed to replicate the same result on iOS Safari, Microsoft Edge (Chromium), and Firefox (which now supports preload).

Cloudflare Workers#

Here’s where I get the chance to play with Cloudflare Workers again. For information on how this is done I recommend reading Andy Davies’ excellent ‘Exploring Site Speed Optimisations With WebPageTest and Cloudflare Workers’ blog post, or you can watch him talk about it at London Web Performance here. If you’d like to see a fully commented sample of my worker code I’ve created a gist here.

Modifying the preloads#

First let’s investigate what happens if we ‘fix’ the preload links and point them to the WOFF fonts referenced in the @font-face rules.

In the resulting waterfall we now see the WOFF fonts preloaded in requests 2-7. This in turn reduces the number of fonts requested by the @font-face rules after the CSS has been parsed.

Modifying the CSS#

Now let’s do the reverse, let’s update the CSS @font-face rules and swap the order so the WOFF2 fonts are first. This will then match the WOFF2 fonts listed in the preload links in the head.

In this waterfall the preload is untouched, but I’ve simply made the WOFF2 fonts come first in the src order like so:

@font-face {
    font-family: MercuryTextG2-Semibold-Pro_Web;
    src: url(.../fonts/MercuryTextG2-Semibold-Pro_Web.woff2) format("woff2"),
    	url(.../fonts/MercuryTextG2-Semibold-Pro_Web.woff) format("woff");
    font-weight: 400;
    font-style: normal;
    font-display: block
}

It’s amazing what such a small change can do to a waterfall.

Effect on performance#

But it’s not a cleaner looking waterfall that’s important, it’s the performance a user sees after the change. So the first thing to note is the impact on the number of bytes downloaded:

Version Total Size (bytes) Original 2,133,401 Fixed preload 1,904,496 Fixed @font-face 1,696,008

Reordering the @font-face rule alone has reduced the number of bytes downloaded by 440KB. That’s a pretty decent saving by simply swapping a few 2’s in the font filename and format().

But where you really see the difference is in the visual progress graph:

Version Visually Complete (ms) Original 8,668 Fixed preload 5,681 Fixed @font-face 5,311

We’ve shaved off almost 3.4 seconds from the visual complete time on a Moto G4 / 3G Fast connection, a 39% reduction. That’s a huge improvement in performance for such a minor change. Your users will really notice that change. If you are interested in the full test results then here’s the WebPageTest comparison view with all three tests.

Is this a problem in the real world?#

So a good question to ask is now: is this a one-off occurrence, or does it happen quite frequently? Well, with December data from the HTTP Archive and the help of Barry Pollard we are able to answer this question:

Mobile (abs) Desktop (abs) Mobile (%) Desktop (%) Download same WOFF / WOFF2 font (one fails) 155,541 132,044 2.17 2.19 Download same WOFF / WOFF2 font (both return 200) 69,805 59,387 0.97 0.99

We see 155,541 (2.17%) sites try to download both WOFF and WOFF2 fonts with the same filename. What most likely happens in these instances is the WOFF2 file fails, so the browser moves onto the WOFF fallback. This still isn’t great for web performance since an extra request / response is made and adds time to the page load and blocks text rendering (I’m over-simplifying here I know). But what’s worse is that we see 69,805 (0.97%) sites successfully double download both WOFF and WOFF2 versions of the exact same font! That’s a lot of unnecessary bytes being downloaded which simply won’t be used. If you are interested in the full dataset for this query you can view it here.

Spotting the preload issue#

So how would you know if this is an issue for your site? Well the easiest thing to do is to look in your browser console. Both Firefox and Chrome will give you a set of warnings if you are preloading assets that aren’t actually used in the page:

If you preload assets that aren't used in the page load the browser will most likely tell you in the console. Warnings from Firefox about the unused fonts.

What if the first src fails#

So let’s now look at what happens with a waterfall when the first source in the @font-face fails. This isn’t related to the White House website but the HTTP Archive data shows that it is happening for approximately 86,000 (1.2%) of sites in the HTTP Archive data. There’s a caveat with this data. The only thing we know is that the browser tries to download both types of font files (WOFF & WOFF2). We also know that one of these fonts doesn’t return a 200 status code (hence the fallback font then loads). But we don’t have an exact figure on if one of the font requests was triggered by a preload, then falling back to @font-face. We also don’t have the data on what source order the fonts are loaded in if using @font-face. So all in all, it’s complicated! But what we do know is that the browser tried to load fonts of both types, and for some reason one of them failed (4xx status code).

What does this look like on a waterfall and what’s the performance impact?

The waterfall & performance impact#

Below you can see the annotated waterfall for a site that requests the WOFF2 font first but fails. This test was again run on a real Moto G4 / 3G Fast connection:

In the waterfall we see lots happening where by both sets of WOFF and WOFF2 fonts are loaded over the lifecycle of the page.

There’s a fair amount going on in this waterfall so let’s step through it:

  • On request 37 we see the TCP negotiation happen for the font domain. Once completed the font request goes out for the WOFF2 font. This whole process takes 2 seconds to complete. It takes ~865 ms to fail. That includes a full round trip to the server and back.
  • On request 41 it takes a full 1.7 seconds to fail. Notice how once failed the browser almost immediately requests the fallback WOFF font (request 46).
  • Notice how this fail / request pattern for all 4 fonts are aligned. Requests 37 & 43, 39 & 45, 40 & 44, and 41 & 46 are all font pairings (WOFF2 vs WOFF).

So in other words the browser must receive a font failure before it can request the fallback. This lack of primary font source is adding 2 seconds to the font load in this test case. What’s worse in the test case above is that the browser ends up downloading the larger of the two font file types. Since WOFF files can be 20-30% larger than WOFF2 files. Ultimately the user gets a double whammy in terms of poor performance: added time to make a successful font request, and more data to download.

So how can you check for this? Well, again I’d look to the browser console. If you notice 404 errors for fonts, I’d put those errors pretty high up your prioritisation list to fix!

Summary#

So in summary, when you are preloading fonts make sure that what is preloaded matches with the src defined in your @font-face rules. Remember the first src the browser finds wins. If these don’t match then your users may be downloading two sets of exactly the same fonts. Make sure you check your browser console for preload warnings and font 404 errors. As both of these issues can seriously impact page performance and adding to the data requirements for the site.


Post changelog:

  • 23/01/21: Initial post published. Thanks to Barry Pollard for the HTTP Archive data and Andy Davies for the Cloudflare Worker debugging.
  • 24/01/21: Added more detail about the performance impact of fonts failing to load, thanks to Barry Pollard for the scenario. Added links for more information about Cloudflare workers. Added info about USDS having no involvement in the White House website. Added a link to the gist with a sample of the cloudflare worker code I used.

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK