"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.
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
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's connection view condenses all the waterfall rows to give a simpler picture showing the fonts (red) are received before the styles (green).
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.
The connection view shows the fonts (red) being loaded after styles (green) but before scripts (orange)
And a visual comparison shows no improvement, as rendering won't begin until the render blocking scripts in the head have executed.
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?