A web font workflow is simple, right? Choose a few nice-looking web-ready fonts, get the HTML or CSS code snippet, plop it in the project, and check if they display properly. People do this with Google Fonts a zillion times a day, dropping its <link>
tag into the <head>
.
Let’s see what Lighthouse has to say about this workflow.

<head>
have been flagged by Lighthouse as render-blocking resources and they add a one-second delay to render? Not great.We’ve done everything by the book, documentation, and HTML standards, so why is Lighthouse telling us everything is wrong?
Let’s talk about eliminating font stylesheets as a render-blocking resource, and walk through an optimal setup that not only makes Lighthouse happy, but also overcomes the dreaded flash of unstyled text (FOUT) that usually comes with loading fonts. We’ll do all that with vanilla HTML, CSS, and JavaScript, so it can be applied to any tech stack. As a bonus, we’ll also look at a Gatsby implementation as well as a plugin that I’ve developed as a simple drop-in solution for it.
What we mean by “render-blocking” fonts
When the browser loads a website, it creates a render tree from the DOM, i.e. an object model for HTML, and CSSOM, i.e. a map of all CSS selectors. A render tree is a part of a critical render path that represents the steps that the browser goes through to render a page. For browser to render a page, it needs to load and parse the HTML document and every CSS file that is linked in that HTML.
Here’s a fairly typical font stylesheet pulled directly from Google Fonts:
@font-face {
font-family: 'Merriweather';
src: local('Merriweather'), url(https://fonts.gstatic.com/...) format('woff2');
}
You might be thinking that font stylesheets are tiny in terms of file size because they usually contain, at most, a few @font-face
definitions. They shouldn’t have any noticeable effect on rendering, right?
Let’s say we’re loading a CSS font file from an external CDN. When our website loads, the browser needs to wait for that file to load from the CDN and be included in the render tree. Not only that, but it also needs to wait for the font file that is referenced as a URL value in the CSS @font-face
definition to be requested and loaded.
Bottom line: The font file becomes a part of the critical render path and it increases the page render delay.

(Credit: web.dev under Creative Commons Attribution 4.0 License)
What is the most vital part of any website to the average user? It’s the content, of course. That is why content needs to be displayed to the user as soon as possible in a website loading process. To achieve that, the critical render path needs to be reduced to critical resources (e.g. HTML and critical CSS), with everything else loaded after the page has been rendered, fonts included.
If a user is browsing an unoptimized website on a slow, unreliable connection, they will get annoyed sitting on a blank screen that’s waiting for font files and other critical resources to finish loading. The result? Unless that user is super patient, chances are they’ll just give up and close the window, thinking that the page is not loading at all.
However, if non-critical resources are deferred and the content is displayed as soon as possible, the user will be able to browse the website and ignore any missing presentational styles (like fonts) — that is, if they don’t get in the way of the content.

The optimal way to load fonts
There’s no point in reinventing the wheel here. Harry Roberts has already done a great job describing an optimal way to load web fonts. He goes into great detail with thorough research and data from Google Fonts, boiling it all down into a four-step process:
- Preconnect to the font file origin.
- Preload the font stylesheet asynchronously with low priority.
- Asynchronously load the font stylesheet and font file after the content has been rendered with JavaScript.
- Provide a fallback font for users with JavaScript turned off.
Let’s implement our font using Harry’s approach:
<!-- https://fonts.gstatic.com is the font file origin -->
<!-- It may not have the same origin as the CSS file (https://fonts.googleapis.com) -->
<link rel="preconnect"
href="https://fonts.gstatic.com"
crossorigin />
<!-- We use the full link to the CSS file in the rest of the tags -->
<link rel="preload"
as="style"
href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap" />
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap"
media="print" onload="this.media='all'" />
<noscript>
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap" />
</noscript>
Notice the media="print"
on the font stylesheet link. Browsers automatically give print stylesheets a low priority and exclude them as a part of the critical render path. After the print stylesheet has been loaded, an onload
event is fired, the media is switched to a default all
value, and the font is applied to all media types (screen, print, and speech).

It’s important to note that self-hosting the fonts might also help fix render-blocking issues, but that is not always an option. Using a CDN, for example, might be unavoidable. In some cases, it’s beneficial to let a CDN do the heavy lifting when it comes to serving static resources.
Even though we’re now loading the font stylesheet and font files in the optimal non-render-blocking way, we’ve introduced a minor UX issue…
Flash of unstyled text (FOUT)
This is what we call FOUT:

Why does that happen? To eliminate a render-blocking resource, we have to load it after the page content has rendered (i.e. displayed on the screen). In the case of a low-priority font stylesheet that is loaded asynchronously after critical resources, the user can see the moment the font changes from the fallback font to the downloaded font. Not only that, the page layout might shift, resulting in some elements looking broken until the web font loads.
The best way to deal with FOUT is to make the transition between the fallback font and web font smooth. To achieve that we need to:
- Choose a suitable fallback system font that matches the asynchronously loaded font as closely as possible.
- Adjust the font styles (
font-size
,line-height
,letter-spacing
, etc.) of the fallback font to match the characteristics of the asynchronously loaded font, again, as closely as possible. - Clear the styles for the fallback font once the asynchronously loaded font file has has rendered, and apply the styles intended for the newly loaded font.
We can use Font Style Matcher to find optimal fallback system fonts and configure them for any given web font we plan to use. Once we have styles for both the fallback font and web font ready, we can move on to the next step.

We can use the CSS font loading API to detect when our web font has loaded. Why that? Typekit’s web font loader was once one of the more popular ways to do it and, while it’s tempting to continue using it or similar libraries, we need to consider the following:
- It hasn’t been updated for over four years, meaning that if anything breaks on the plugin side or new features are required, it’s likely no one will implement and maintain them.
- We are already handling async loading efficiently using Harry Roberts’ snippet and we don’t need to rely on JavaScript to load the font.
If you ask me, using a Typekit-like library is just too much JavaScript for a simple task like this. I want to avoid using any third-party libraries and dependencies, so let’s implement the solution ourselves and try to make it is as simple and straightforward as possible, without over-engineering it.
Although the CSS Font Loading API is considered experimental technology, it has roughly 95% browser support. But regardless, we should provide a fallback if the API changes or is deprecated in the future. The risk of losing a font isn’t worth the trouble.
The CSS Font Loading API can be used to load fonts dynamically and asynchronously. We’ve already decided not to rely on JavaScript for something simple as font loading and we’ve solved it in an optimal way using plain HTML with preload and preconnect. We will use a single function from the API that will help us check if the font is loaded and available.
document.fonts.check("12px 'Merriweather'");
The check()
function returns true
or false
depending on whether the font specified in the function argument is available or not. The font size parameter value is not important for our use case and it can be set to any value. Still, we need to make sure that:
- We have at least one HTML element on a page that contains at least one character with web font declaration applied to it. In the examples, we will use the
but any character can do the job as long it’s hidden (without usingdisplay: none;
) from both sighted and non-sighted users. The API tracks DOM elements that have font styles applied to them. If there are no matching elements on a page, then the API isn’t be able to determine if the font has loaded or not. - The specified font in the
check()
function argument is exactly what the font is called in the CSS.
I’ve implemented the font loading listener using CSS font loading API in the following demo. For example purposes, loading fonts and the listener for it are initiated by clicking the button to simulate a page load so you can see the change occur. On regular projects, this should happen soon after the website has loaded and rendered.
Isn’t that awesome? It took us less than 30 lines of JavaScript to implement a simple font loading listener, thanks to a well-supported function from the CSS Font Loading API. We’ve also handled two possible edge cases in the process:
- Something goes wrong with the API, or some error occurs preventing the web font from loading.
- The user is browsing the website with JavaScript turned off.
Now that we have a way to detect when the font file has finished loading, we need to add styles to our fallback font to match the web font and see how to handle FOUT more effectively.
The transition between the fallback font and web font looks smooth and we’ve managed to achieve a much less noticeable FOUT! On a complex site, this change would result in a fewer layout shifts, and elements that depend on the content size wouldn’t look broken or out of place.
What’s happening under the hood
Let’s take a closer look at the code from the previous example, starting with the HTML. We have the snippet in the <head>
element, allowing us to load the font asynchronously with preload, preconnect, and fallback.
<body class="no-js">
<!-- ... Website content ... -->
<div aria-visibility="hidden" class="hidden" style="font-family: '[web-font-name]'">
/* There is a non-breaking space here */
</div>
<script>
document.getElementsByTagName("body")[0].classList.remove("no-js");
</script>
</body>
Notice that we have a hardcoded .no-js
class on the <body>
element, which is removed the moment the HTML document has finished loading. This applies webfont styles for users with JavaScript disabled.
Secondly, remember how the CSS Font Loading API requires at least one HTML element with a single character to track the font and apply its styles? We added a <div>
with a
character that we are hiding from both sighted and non-sighted users in an accessible way, since we cannot use display: none;
. This element has an inlined font-family: 'Merriweather'
style. This allows us to smoothly switch between the fallback styles and loaded font styles, and make sure that all font files are properly tracked, regardless of whether they are used on the page or not.
Note that the
character is not showing up in the code snippet but it is there!
The CSS is the most straightforward part. We can utilize the CSS classes that are hardcoded in the HTML or applied conditionally with JavaScript to handle various font loading states.
body:not(.wf-merriweather--loaded):not(.no-js) {
font-family: [fallback-system-font];
/* Fallback font styles */
}
.wf-merriweather--loaded,
.no-js {
font-family: "[web-font-name]";
/* Webfont styles */
}
/* Accessible hiding */
.hidden {
position: absolute;
overflow: hidden;
clip: rect(0 0 0 0);
height: 1px;
width: 1px;
margin: -1px;
padding: 0;
border: 0;
}
JavaScript is where the magic happens. As described previously, we are checking if the font has been loaded by using the CSS Font Loading API’s check()
function. Again, the font size parameter can be any value (in pixels); it’s the font family value that needs to match the name of the font that we’re loading.
var interval = null;
function fontLoadListener() {
var hasLoaded = false;
try {
hasLoaded = document.fonts.check('12px "[web-font-name]"')
} catch(error) {
console.info("CSS font loading API error", error);
fontLoadedSuccess();
return;
}
if(hasLoaded) {
fontLoadedSuccess();
}
}
function fontLoadedSuccess() {
if(interval) {
clearInterval(interval);
}
/* Apply class names */
}
interval = setInterval(fontLoadListener, 500);
What’s happening here is we’re setting up our listener with fontLoadListener()
that runs at regular intervals. This function should be as simple as possible so it runs efficiently within the interval. We are using the try-catch block to handle any errors and catch any issues so that web font styles still apply in the case of a JavaScript error so that the user doesn’t experience any UI issues.
Next, we’re accounting for when the font successfully loads with fontLoadedSuccess()
. We need to make sure to first clear the interval so the check doesn’t unnecessarily run after it. Here we can add class names that we need in order to apply the web font styles.
And, finally, we are initiating the interval. In this example, we’ve set it up to 500ms, so the function runs twice per second.
Here’s a Gatsby implementation
Gatsby does a few things that are different compared to vanilla web development (and even the regular create-react-app tech stack) which makes implementing what we’ve covered here a bit tricky.
To make this easy, we’ll develop a local Gatsby plugin, so all code that is relevant to our font loader is located at plugins/gatsby-font-loader
in the example below.
Our font loader code and config will be split across the three main Gatsby files:
- Plugin configuration (
gatsby-config.js
): We’ll include the local plugin in our project, list all local and external fonts and their properties (including the font name, and the CSS file URL), and include all preconnect URLs. - Server-side code (
gatsby-ssr.js
): We’ll use the config to generate and include preload and preconnect tags in the HTML<head>
usingsetHeadComponents
function from Gatsby’s API. Then, we’ll generate the HTML snippets that hide the font and include them in HTML usingsetPostBodyComponents
. - Client-side code (
gatsby-browser.js
): Since this code runs after the page has loaded and after React starts up, it is already asynchronous. That means we can inject the font stylesheet links using react-helmet. We’ll also start a font loading listener to deal with FOUT.
You can check out the Gatsby implementation in the following CodeSandbox example.
I know, some of this stuff is complex. If you just want a simple drop-in solution for performant, asynchronous font loading and FOUT busting, I’ve developed a gatsby-omni-font-loader plugin just for that. It uses the code from this article and I am actively maintaining it. If you have any suggestions, bug reports, or code contributions, feel free to submit them on on GitHub.
Conclusion
Content is perhaps the most component to a user’s experience on a website. We need to make sure content gets top priority and loads as quickly as possible. That means using bare minimum presentation styles (i.e. inlined critical CSS) in the loading process. That is also why web fonts are considered non-critical in most cases — the user can still consume the content without them — so it’s perfectly fine for them to load after the page has rendered.
But that might lead to FOUT and layout shifts, so the font loading listener is needed to make a smooth switch between the fallback system font and the web font.
I’d like to hear your thoughts! Let me know in the comments how are you tackling the issue of web font loading, render-blocking resources and FOUT on your projects.
References
- Eliminate render-blocking resources (web.dev)
- Optimize WebFont loading and rendering (web.dev)
- Render Blocking CSS (Google Web Fundamentals)
- The Fastest Google Fonts (CSS Wizardry)
- CSS Basics: Fallback Font Stacks for More Robust Web Typography (CSS-Tricks)
- CSS Font Loading API (MDN)
- Font style matcher
Thanks for this. As always, it made sense until you started mentioning Gatsby :)
Glad you’ve liked the article. I’ve wanted to cover Gatsby implementation as a bonus, so the idea and code up until that final section can be applied to the framework and tech stack of your choice.
This is a coincidence, I spent most of yesterday tinkering with exactly this topic after being penalized by Pagespeed Insights. Then after having an email convo about implementing this kind of topic with the developer of my Joomla framework, he pointed me to this article published only yesterday! It seems great minds think alike :). But, on a more serious note, finding these tweaks to improve pageload scores on Google will soon benefit all. Especially as the impending Google Algorythum change to notify users of slow websites (in the same way as it does with non https pages, ie a blank page with a warning on it, and a small ‘are you sure?’ link at the bottom) is going to affect millions of websites. Tweaks like this are going to make all the difference.
Thanks a lot for taking the time to put this together! I’d never heard the term FOUT before, but I’ve wondered how to go about fixing it for quite some time. I’ll definitely be bookmarking this page to refer to on my next project.
Thank you very much. Glad to hear that you’ve enjoyed the article and plan to use FOUT handling on your next project!
The font loading ‘observer’ is quite clever, but I’m not really sure why you use this in first place, when
document.fonts.load(...).then(...)
exists. That removes the HTML line with the and simplifies code a lot.Valid point, but consider it this way:
Font loading – it’s “essential” for website presentation. We’d like to keep it as close to native as possible, so I went with HTML with JavaScript fallback. I also don’t want it to depend on an experimental API that could change at any time. I need to provide the HTML fallback anyway if anything happens with the spec or if the browser doesn’t support the API.
FOUT handling – useful, but not essential. If Font API changes or is not supported, the only issue is some minor visual jank. Not a real issue. So I can let JavaScript handle it.
So I’d recommend doing it this way until spec is final and non-experimental.
My thoughts: I don’t like it.
I don’t like skeleton loaders, fonts or images or anything else popping around after the content appears – it’s distracting.
Solve your performance problems instead of inventing clever workarounds. Pick smaller fonts. Fewer fonts. Preload any fonts visible above the fold. Use HTTP/2. Base-64 encode and inline if you have to.
This whole story about “perceived performance” is an excuse to avoid solving underlying technical or design issues. It’s an enticing opportunity to flex your creative muscle and come up with “clever” workarounds.
But none of it is necessary. Good web fonts are optimized for the web. Good design generally does not use more than two typefaces. Good implementations optimize by compressing, inlining, preloading, whatever it takes to deliver the intended experience.
Just my opinion, but I see so many of these techniques as distracting from the real problems, while always adding at least some unnecessary complexity.
Solving performance problems the traditional way may not be new or exciting or fun – but I don’t think anyone should reach for “perceived performance” workarounds before all other options are exhausted.
Hi Rasmus, thanks for your input.
I agree that you should fix the underlying issues and dig into the system rather than add complexity. That is why I wanted to keep my implementation as close to native functionalities as possible (no dependencies, no libs, no hacks).
I certainly don’t consider it “the best way to load fonts & handle FOUT”, but rather a stepping stone to a better solution and a quick solution that works for most scenarios.
Optimizing web fonts is a complex topic (as is image optimization or any web optimization). I actually checked out how font loading has been done for CSS-tricks and it is a doozy: https://www.zachleat.com/web/css-tricks-web-fonts/
I think your solution is ideal, but it very much depends on the scenario. Sometimes, a client uses a licensed font that is loaded from an external server and they don’t have any control over the font file itself. Sometimes these optimization tasks do not fit into the client’s budget or deadline. And the complex solution does require some forethought and dev experience to pull it off safely and without any future issues.
Lastly, I think this fixes minor issues that browsers have – they don’t display content while the font file is loading. I find it unnecessary that the content display depends on the font file, and I wanted to fix FOUT that comes with async loading.
Anyway, I think the approach should depend on the scenario, project complexity, dev skills & experience, and available time and budget.
Interesting approach. But using setTimeout/setInterval can have some timing inconsistencies and lag depending on various factors thus possibly prolonging the check for the fonts to load, since we are talking about a fast occurring visual change this might be quite impactful. https://hackwild.com/article/web-worker-timers/. Also, have you maybe compared the results of your technique with just using font-display:swap? Thanks for the article!
Hi Mihovil, thanks for the comment.
As for the “setInterval” performance, I did some research on the topic. Generally, you should only have a simple function inside the interval so we don’t run into a major performance bottleneck. We’re using a simple native API check (not depending on any HTTP requests or some complex operation), so we’re good on that point. It’s not an issue if the timer is not accurate, we just want to run it occasionally and see if the font is loaded. Hope this makes the implementation a bit clearer.
As for the “font-display: swap”, that is a good approach, but the problem is that you still notice the swap between the fallback font and the main font. The width and height differences between the main font and fallback font could cause some annoying layout shifts or make some elements look broken, depending on the layout and the screen size. With the FOUT approach, we can detect when the main font has finished loading and toggle a CSS class (font styles) so the layout shift doesn’t happen and swapping is not noticeable.
Great article Adrian! Didn’t know that font file can increases the page render delay.
Thank you. Glad you’ve enjoyed it!
I’ve found that self-hosting my fonts solves pretty much all the problems web fonts can create. Handy tool to help: https://google-webfonts-helper.herokuapp.com/fonts
Thanks for this very timely article Adrian! In your example, the font sizes are all set in pixels. In the sites I build I use variable font sizing formulas, using calc() or clamp(). I’m curious if you have any suggestions how to tackle FOUT when the font sizes are all variable?
Example:
font-size: calc(1.3125rem + (75vw - 15rem)/65.375);
Say the header has a menu, and then below that there’s a hero section. The text for the menu items, as well as the hero will all have varying font sizes, not fixed pixel values. So it makes the font-swapping rather challenging to implement accurately and with as little layout shifting as possible.
Hi Zach, thank you. That is a great question.
Font style matching is tricky, so I guess the variable fonts makes this even trickier. I haven’t tried this out myself, but my best guess is to use the font style matcher and find matches for min font size and max font size from the calc snippet you’ve provided. The same should be done with the letter spacing, word spacing, line-height… basically everything typography-related. You’ll end up with a bunch of sets of calc functions for both the fallback font and main font.
In an ideal case, the fallback font params should linearly match the params of the main font, so the changes in the resolution are consistent and FOUT is handled properly.
Let me know the results if you decide to try this out.
The example code shows the property aria-visibility. But this tag doesn’t exist (at least I didn’t find anything in the specification). I feel like it should be aria-hidden=”true” instead.
Thank you for the article, it helped a lot! Let me add my two cents. )
Why don’t you use document.fonts.ready.then() which seems to have the same browser coverage as check()? No need for setInterval()!
Also, you can use .fonts-loader::before { content: “\00a0”; font-family: ‘Merriweather’; } to keep html a little bit cleaner. “\00a0” – is a
Thank you for the article! One point isn’t completely clear to me. Where do you include the script with setInterval? At the end of the body tag, after the script which removes no-js class, or before? (Or in the head tag?)
A lot of these articles focus on Google fonts. What if a site uses Adobe fonts? Any tricks for those? How can you use a tool like font style matcher with Adobe fonts? I am new to this optimization stuff.
Great write-up. I recently disabled custom fonts on my blog, and …. I just can’t stand the look of it anymore. I don’t remember hating Helvetica and Arial so much when I was younger. But now, it just looks janky as a long-form font. I’m not too concerned about the FOUT – I just want to have a nice looking font eventually load.