Andy Davies

Web Performance Consultant

Preloading Fonts and the Puzzle of Priorities

"Consider using <link rel=preload> to prioritize fetching resources that are currently requested later in page load" is the seemingly simple advice Lighthouse gives.

Preload is a trade-off – when we explicitly increase the priority of one resource we implicitly decrease the priority of others – and so to work effectively preload requires authors to identify the optimal resources to preload, browsers to request them with optimal priorities and servers to deliver them in optimal order.

Some resources are discovered later than others, for example resources injected by a script, or background images and fonts that are only discovered when the render tree is built.

Preload instructs the browser to download a resource even before it discovers it but so there may be performance gains by using <link rel=preload to download resources earlier than normal.

Preload also allows us to decouple download and execution, for example the Filament Group suggest using it to load CSS asynchronously

I’ve being using preload with clients over the last few years but I have never been completely satisfied with the results. I’ve also seen some things I hadn’t quite expected in page load waterfalls so decided to dig deeper.

When Should Fonts be Loaded?

As most browsers delay rendering text until the relevant font is available, loading fonts sooner has become one of commonly suggested use cases for preload.

And based on my exploration of the HTTP Archive data it's the most frequently used case too.

By default fonts are loaded late as the browser only discovers them when it's building the render tree i.e. after the styles have been downloaded and any blocking scripts in the head have been downloaded and executed too.

The waterfall below shows the default behaviour with fonts discovered late and then having to compete with the images.

Ideally we want the fonts downloaded much sooner, most likely after the render blocking elements in the head – this is the point where the browser starts adding the body contents to the DOM and where the fonts will be needed.

WebPageTest waterfall showing default and ideal prioritisation of fonts loading

Default font loading behaviour - WebPageTest, Dulles, Chrome, 3G Fast

Preloading fonts isn't the only option when it comes to reducing the 'Flash of Invisible Text', the CSS font-display property can be used to change the blocking behaviour but reducing the time it takes for the web fonts to replace the fallback font should still improve visual performance.

Although I'm focusing on fonts, many of the observations apply to the preload of other resource types too.

Creating a Test Page

To explore further I need a test case, something that approximates some of the pages retailers and other sites might have.

Rather than start from scratch I based the test case on the Electro ecommerce template from ColorLib with a few changes:

  • switched from Google Fonts to self-hosted ones (containing just Latin glyphs).
  • removed unused glyphs to reduce fontawesome’s size added a performance mark to record when the fonts were loaded (using document.fonts.ready).
  • moved some blocking scripts from the foot of the page into the head element and added a script loaded with async, and another with defer attributes.

The page has (at least) a few shortcomings when compared to some of the real world pages I see:

  • only makes 35 requests:
  • is only around 1,500 lines long - which is pretty short compared to most retailers sites
  • doesn’t contain any third-party content
  • there are no long running scripts that affect interactivity

The test case has four fonts – three weights of Montserrat, and an icon font – all weighing in at just under 100kB, which is in-line with the median size and number of requests for fonts based on HTTP Archive data

I created several variants of the test page, and if you want to take a deeper look they’re all on GitHub complete with descriptions – https://github.com/andydavies/test-rel-preload

The test pages were hosted on Github, Cloudfront, and AWS (using h2o as a server), and tested in Chrome and Canary, using WebPageTest in Dulles on both 3G Fast and Cable connections.

There are links to all the results on GitHub too.

Default Behaviour vs Preloaded Fonts

For the first test I compared fonts loaded using the default browser behaviour with a variant that uses <link rel=preload… at the top of the head to preload each font.

As the filmstrip below shows, the page with default behaviour (top) starts rendering first but suffers from text that doesn’t become visible until 4.9s.

The page that preloads the fonts (bottom) starts rendering 0.4s later, but text is visible as soon as it starts rendering

WebPageTest filmstrip illustrating delay caused by preloading fonts

Comparison with and without preloaded fonts, WebPageTest, Dulles, Chrome, 3G Fast

The waterfall for the page with preloaded fonts illustrates why it’s slower to start rendering – although the fonts are given a medium priority, the requests for them are dispatched immediately, before the requests for the higher priority stylesheets and then the responses for the fonts delay the stylesheets.

WebPageTest waterfall illustrating preloading fonts delaying CSS

WebPageTest's connection view condenses all the waterfall rows to give a simpler picture showing the fonts (red) are received before the styles (green).

WebPageTest connection view illustrating preloading fonts delaying CSS

As the browser can’t do anything with the fonts until it has the stylesheets this really isn’t the behaviour we want, so why is it happening?

Firstly, Chrome sends the requests for the resource to be preload immediately, while the requests for the other resources wait for AppCache to be initialised – https://bugs.chromium.org/p/chromium/issues/detail?id=788757 – so even moving the preload directives to the bottom of the header or into the body don’t help.

Then GitHub’s servers don’t reprioritise the higher priority requests above the medium priority ones and so carry on serving the fonts even when the requests for the stylesheets are received.

To be fair to GitHub, prioritising early requests is hard – if the connection is idle a server is going to start responding as soon as it receives a request, and it may not need interrupt that response until a request with a higher priority is received.

Hopefully the Chrome team will fix the AppCache delay soon but in the meantime there are some other things we can try.

Switching HTTP/2 Servers

Recently Pat Meenan and I started tracking how well servers, CDNs etc., support HTTP/2 priorisation (https://github.com/andydavies/http2-prioritization-issues) so what if we switched to a server (h2o) that’s known to prioritise well?

I installed h2o 2.3.0-beta1 on a t2.micro instance in AWS US-East running Ubuntu 18.04 using BBR for congestion control and qdisc set to fq.

Although the improvement in prioritisation now results in the stylesheets being sent before the fonts, both fonts and render blocking scripts are requested with a medium priority so the scripts still get stuck behind the fonts.

WebPageTest waterfall illustrating preloading fonts delaying JS

The connection view shows the fonts (red) being loaded after styles (green) but before scripts (orange)

WebPageTest connection view illustrating preloading fonts delaying JS

And a visual comparison shows no improvement, as rendering won't begin until the render blocking scripts in the head have executed.

WebPageTest filmstrip illustrating delay caused by preloading fonts

Comparison of default font loading behaviour and preloading front using h2o as a server - WebPageTest, Dulles, Chrome, 3G Fast

NOTE: In general, the delay to rendering was smaller with a faster network connection – 100ms on Cable for the pages hosted on GitHub. But using h2o, even on a cable connection, the page with preloaded fonts was noticeably slower (0.3s) to start rendering – the bandwidth chart showed throughput stalling after the resources in the head have been delivered and at the moment I'm unsure whether this is a Chrome issue, a h2o issue, or a combination of both.

Priority Hints to the Rescue?

Priority Hints is recent proposal that allows authors to hint how important a resource is and it’s available behind a flag in Chrome Canary.

If we added importance=”low” to the preload elements, would it encourage Canary to prioritise the fonts at a lower priority than the blocking scripts?

<link rel="preload" importance="low" href="fonts/Montserrat-Regular.woff2" as="font" crossorigin/>

So close… and yet so far…

Interestingly the two earlier fonts are given a priority of lowest, and the later ones a priority of highest – maybe the browser has built enough of the DOM to discover it needs them and elevates their priority?

WebPageTest waterfall illustrating preloading fonts and priority hints

The connection view re-enforces the importance of sending high priority requests first so they don't get stuck behind lower priority ones.

WebPageTest connection view illustrating preloading fonts delaying JS

Comparison of default font loading behaviour and declarative preloading using Priority Hints - WebPageTest, Dulles, Canary, 3G Fast

But as priority hints are still an experiment it's behind a flag, so even though it shows promise most visitors won't be able to take advantage of it yet.

Delaying the Preload Hint

As mentioned earlier, resources injected using a script are hidden from the browser until the script executes, and we can use this behaviour to delay when the browser discovers the preload hint.

Using the snippet below I injected the rel=preload elements into the DOM and tested with the snippet positioned both before, and after the external scripts.

<script>
  (function () {
      var fontsToPreload = [
          {"href": "fonts/fontawesome-webfont.woff2?v=4.7.0", "type": "font/woff2"},
          {"href": "fonts/Montserrat-Medium.woff2", "type": "font/woff2"},
          {"href": "fonts/Montserrat-Regular.woff2", "type": "font/woff2"},
          {"href": "fonts/Montserrat-Bold.woff2", "type": "font/woff2"}
      ];
  
      var fragment = document.createDocumentFragment();
      for(font of fontsToPreload) {
          var preload = document.createElement('link');
          preload.rel = "preload";
          preload.href = font.href;
          preload.type = font.type;
          preload.as = "font";
          preload.crossOrigin = "anonymous";
          fragment.appendChild(preload)
      }
      document.head.appendChild(fragment);
  })();
</script>

(Yes… perhaps the script could do with a bit of a tidy up)

And finally, we have a filmstrip that hints at preload's promise!

The page in the bottom row has the above snippet just before the external blocking scripts, it starts rendering at the same time the default page (top), and renders text 0.3s sooner than the page the uses declarative preload statements (middle).

WebPageTest filmstrip comparing default font loading, with preloaded fonts and script injected preload elementillustrating delay caused by preloading fonts

The waterfall shows the fonts being loaded later.

WebPageTest waterfall illustrating inserting preload element using a script

And the connection view confirms the styles and scripts are downloaded before the fonts.

WebPageTest connection view illustrating inserting preload element using a script

Comparison of default font loading behaviour, declarative preloading and script injected preloading - WebPageTest, Dulles, Chrome, 3G Fast

It's not a perfect result, ideally I'd like to see the fonts retrieved before scripts that are async, or deferred but perhaps we have a winner?

What About Other Browsers?

Only Chrome and Safari currently support <link rel=preload… and so far, all my examples have used Chrome (or Chrome Canary), so what about Safari?

As Inspector shows, it appears Safari prioritises the declaratively preloaded fonts appropriately – they are loaded after the stylesheets and scripts in the <head> so they’re not delaying these critical resources.

Safari Inspector showing prioritisation of preloaded fonts

(I checked the behaviour separately using WebPageTest, Resource Timing, and server logs too.)

But if as highlighted earlier, Chrome's behaviour leads to declarative preloads being downloaded too early, what about the scripted approach that worked so well in Chrome?

Unfortunately, inserting the preload elements using a script frequently leads to Safari 'double downloading' the specified resources.

The requests also get dispatched after the lower priority images so we are reliant on the server to prioritise them effectively too.

Safari Inspector showing double download when preloaded elements inserted via script

When it comes to examining network traffic, Safari isn’t as helpful as Chrome – there’s no equivalent of Chrome’s netlog, and we can’t capture TLS keys and then use Wireshark or Dan DeMeyer’s h2vis to explore the traffic – and I really miss these options.

Theses options really are useful to independently check what's happening at the network layer, for example, in this case even though Inspector shows a double download, Resource Timing data doesn't and I eventually resorted to server logs to verify the double downloads.

If declarative preloading is 'broken' in Chrome, and inserting the preload elements via a script results in double downloads in Safari, what approach should we take?

Closing Thoughts

It will be interesting to revisit these tests once hopefully both Chrome fixes the AppCache delay, and Safari fixes the double download issue.

I'm particularly interested in whether the fixes will change the importance of HTTP/2 prioritisation for these use-cases.

There's no doubt preload has the danger to be a 'foot gun' and Chrome's AppCache issue combined with HTTP/2 servers priority challenges really don't help.

But does that mean we should avoid it?

Fonts

Before considering preload for fonts we should go back to basics, if you can't (or don't want to) switch to system fonts there are often opportunities to reduce the size of self-hosted fonts:

  • reduce the number of web fonts in use.
  • remove glyphs that aren't going to be needed via subsetting
  • subsetting is really important if you're using an icon font, you probably don't need to ship all 75kB of fontawesome to your visitors
  • encode fonts as woff2, woff for older browsers (and maybe ttf or eot for really old browsers if you can't just rely on a default font for them)

Everything Fonts Subsetter is handy for manipulating text fonts (I also use Glyphs Mini), and I tend to use the font tools that Bram Stein curates for encoding.

  • use the CSS font-display property with a value of swap to enable the browser to start rendering text sooner
  • include a unicode range in the @font-face declaration

Zach Leatherman's written more on font optimisation than I probably ever will, so I'd suggest reading his Comprehensive Guide to Font Loading too.

Preloading Fonts

Given the issues I outlined earlier, should we even consider preloading fonts?

I suspect even with Chrome's current sub-optimal behaviour there's a case for preloading one or maybe two critical fonts.

But don't take my word for it, test it for yourself as your traffic mix e.g. Safari vs Chrome, the choice of server and your page make up will influence the outcome:

  • if you've got a high proportion of Safari visitors, then perhaps Chrome's behaviour isn't important
  • if you're using servers with poor support for HTTP/2 optimisation such as CloudFront or IIS then the outcome might be very different to using Akamai or Cloudflare.
  • my test pages had multiple external styles, and blocking scripts – pages with more or less render blocking resources may behave differently.

I skipped some optimisation opportunities in my tests, for example what if I'd just preloaded Montserrat Regular, and left the other fonts to load as normal?

What if I had fewer stylesheets or blocking scripts in the head how would these have affected the outcomes?

Preloading is also a first impression optimisation – it should only apply to the first hit in a visitor's session, after that the fonts should come from the local browser cache.

Given first impressions tend to be uncached (and so slower) perhaps there's an opportunity to avoid preload and speed up the first view using font-display:optional or 'font-display: fallback`.

Preloading Other Resource Types

Querying the HTTP Archive data shows varied use cases for preload including font loading, asynchronous CSS loading, scripts, images, right though to a site that seemed to preload every resource (40ish!!!)

As I stated at the start preload is a trade off and given the high priority (at least in Chrome) preloaded resources are implicitly given I worry about what other high priority resources are being delayed.

I think the worst case I've seen is a site where stylesheets were blocked for EIGHT seconds while a video was being preloaded (the video took 20 seconds to load)

Although there's some interesting differences between Chrome and Safari's prioritisation of async, deferred, and foot of the page scripts, browsers are pretty good at prioritising resources they can easily discover.

I doubt there's any benefit to preloading the easily discoverable resources but wonder if some scripts inserted using a tag manager might benefit from it.

Given the issues I've seen with fonts, and some of the tests based on HTTP Archive data I'm pretty cautious about using preload, I think there's a danger it does more harm that good.

Further Research

I've only scratched the surface, there's further opportunities to understand how the number of number of preloaded fonts (and the order they're loaded in) affects the visitor experience.

Other questions include when should we (if at all) preload images, stylesheets scripts etc., and how will priority hints interact with them.

Preload as a HTTP header is often mentioned but doesn't seem to be greatly used, I've concerns that it's an even bigger foot gun than the link element but I can still see use cases e.g. bootstrapping an SPA, or third-parties using a scout script might benefit from the scout script using preload headers to load other resources early (as Google Fonts does with a preconnect hint for example)

Chrome and Safari's prioritisation of async, deferred and foot of the page scripts varies but which one is best and when?

These are some of the research cases I thought of while writing this but I'm sure there's more out there!

References / Further Reading

Test pages and results used in this post

H2 Prioritisation Tracker

H2vis

Chrome AppCache / Prioritisation issue

Resource Hints

Priority Hints proposal

Median size and number of requests for fonts based on HTTP Archive data

Zach Leatherman's A Comprehensive Guide to Font Loading

(EDIT: 13th Feb 2019 - Fixed typos, and added point on page construction to conclusions)

Comments