Avoiding FOUT with Async CSS

Fighting blocking resources and the fabled flash-of-unstyled-text with Adobe Fonts.

Published
Last updated

Font-loading is a solved problem. We have a native API and great support for font-display — both from browsers and foundries.

However, while watching Tim Kadlec and Chris Coyier talk through performance improvements on CSS-Tricks, I was reminded of how damaging the default font-loading strategy often is. On CSS-Tricks, for instance, one of the biggest offenders was the render-blocking stylesheet provided by Hoefler & Co’s hosted font solution.

The real issue isn’t with H&Co., (although they remain especially problematic), but with including any third-party render-blocking resource on your page. Take the (better-behaved) Adobe Fonts, for instance, and their recommended drop-in:

<link rel="stylesheet" href="https://use.typekit.net/:id.css">

It’s simple, but incredibly damaging for performance. Forget about fonts — including this tag in your <head /> blocks the entire page from rendering until it (and any @import’s inside it) have loaded. The stylesheets themselves may be minuscule, but the network overhead is anything but.

One workaround would be proxying the CSS and serving a cached copy from your own domain, circumventing the additional connection costs. Unfortunately, besides assuming that the underlying font locations remain static, this is almost certainly a TOS violation.

So what’s the solution? Load the stylesheet asynchronously.

The Filament Group’s media hack remains the easiest method:

<link rel="stylesheet" href="https://use.typekit.net/:id.css" media="print" onload="this.media='all'">

Voila, up to four seconds shaved off a cold FCP on slow 3G.

Return of the FOUT

Of course, nothing is free. We’ve introduced a FOUT, a momentary ‘flash’ of a fallback font before our custom one has loaded.

The problem with our async solution is that we’re always going to render the page before we load our @font-face definitions. Every page view will now get a FOUT, not because of the fonts, but because of the method we’re using to include the CSS responsible for defining them:

Of course, if you’re writing an isomorphic or single-page app then this isn’t a problem, since subsequent navigation occurs on the client. For traditional server-rendered (or static) sites, however, it can be quite jarring.

Conditionally async

What if, instead of always loading the CSS asynchronously, we change the strategy based on if we’ve already loaded it once before:

  • If we haven’t loaded the CSS before, load it asynchronously.
  • If we have loaded the CSS before, load it synchronously.

Provided that the asset has already been downloaded and cached, we’ll pay no penalty for including it as a blocking resource. The CSS will be pulled from the disk-cache and applied immediately, FOUT-free:

Here’s an example client-side implementation:

<!-- Best to include this inline --> <script> const fontKey = "fontLoaded"; const stylesheet = "https://use.typekit.net/:id.css"; // Once the font has loaded: // - Set the media type, thereby applying the CSS. // - Store a boolean indicating we've already loaded the font. function onFontLoad(script) { script.media = "all"; sessionStorage.setItem(fontKey, "true"); } // If we've loaded it once before, load the CSS in a blocking way. // Otherwise, we load it asynchronously. const styleTag = sessionStorage.getItem(fontKey) ? `<link rel="stylesheet" href="${stylesheet}">` : `<link rel="stylesheet" href="${stylesheet}" media="print" onload="onFontLoad(this)">`; document.write(styleTag); </script>

Since it’s not possible to append a blocking script or stylesheet to the DOM directly, I’ve used document.write to author the tag. It’s worth noting that while Chrome is rallying against a similar use-case for scripts, <link /> still works without a hitch.

Note that this assumes that the browser will pull the asset from the disk-cache on subsequent views, which will not be the case if the CSS has Cache-Control headers to the contrary (unfortunately the case for H&Co.).

Naive over-engineering

I’ve not used this in production, but it does seems to do the trick. Shoot me an email if you have an alternative solution. The takeaways are, if nothing else:

  • Host your own fonts if you can afford to.
  • Never put a 3rd party blocking resource in the critical path. In the case of H&Co., where the cache headers prevent the above selective-loading technique, either pony up for the self-hosted package or learn to live with the FOUT.

← Home