I help companies find and fix site-speed issues. Performance audits, training, consultancy, and more.
Written by Harry Roberts on CSS Wizardry.
N.B. All code can now be licensed under the permissive MIT license. Read more about licensing CSS Wizardry code samples…
Disclaimers:
It all started, as these things often do, with a waterfall:
I was doing some cursory research and running a few tests against a potential client’s site so as to get a good understanding of the shape of things before we were to work together. Even in just the first 10 entries in the waterfall chart above, there were so many fascinating clues and tells that were made immediately apparent. I was struck by some interesting behaviour happening in the very early stages of the page-load lifecycle. Let’s pick it apart…
cloud.typography.com
, has very high connection overhead
(405ms in total), surprisingly large
TTFB
(210ms), and returns a 302
anyway. What’s going on here? Where are we getting redirected to?
fonts.[client].com
: Note a Location:
https://fonts.[client].com/[number]/[hash].css
header.[client].com
to use
the font service? And why is the file so huge?!
302
, forwarding the request to a self-hosted CSS file that is
optimised specifcally for your browser, OS, and UA (Google Fonts do
something similar). The CSS file(s) that we host is provided by
Cloud.typography.
403
response.So, what are the performance implications of all of this?
First up, even though the request to cloud.typography.com
returns a 302
response with a text/html
MIME type, its outgoing request is for CSS. This
makes complete sense as the request is invoked by a <link rel="stylesheet" />
.
We can further verify this by noting the presence of an Accept:
text/css,*/*;q=0.1
request header. Put simply, despite not ‘being’ CSS, this
request sits on our Critical Path and therefore blocks rendering.
To further exacerbate the problem, the 302
response has a Cache-Control:
must-revalidate, private
header,
meaning that we will always make an outgoing request for this resource
regardless of whether or not we’re hitting the site from a cold or a warm cache.
Although this response has a 0B filesize, we will always take the latency hit on
every single page view (and this response is basically 100% latency). On mobile
connections, this can amount to whole seconds of delays, all sat on the Critical
Path.
Next up, we get sent to fonts.[client].com
, which introduces yet more latency
for the connection setup. (Please note that this phenomenon is specific to the
way this company has implemented their own assets and has nothing at all to do
with Cloud.typography.) Because this response is CSS, our critical request chain
remains unbroken—this is all work taking place on the Critical Path.
Once we’ve dealt with the connection overhead, we begin downloading a behemoth CSS file (271.3KB) that is packed with all of the fonts for the project encoded in Base64 data URIs. Base64 is absolutely terrible for performance, and, particularly where fonts are concerned, has the following issues:
font-display
can’t work if there are no fonts; the Font Loading API is
useless if there are no fonts. Technically, what we have here is a CSS
problem, not a font problem.The practical upshot of the above is that we have 1,363ms of render blocking CSS on our Critical Path for a first-time visit on a cable connection. A repeat view still cost us 280ms:
The final nail in the coffin is the fact that, because there are no font files, the browser is unable to employ any kind of decision about how to handle missing typefaces. Most browsers will display invisible text for three seconds and, if the webfont doesn’t make it onto the device in that time, will instead display fallback text. Once the webfont does arrive, we then swap to displaying that. This means that, once the page has started rendering, the user will go for, at most, three seconds unable to read any content.
This all only works if there are actual font files to be negotiated. Because Cloud.typography’s fonts are actually just more CSS, the browser is unable to discern the two, and thus cannot offer a three-second grace period. This means that there’s a theoretically infinite period where we don’t just block the rendering of text, but we block the rendering of the whole page.
If you’re interested in seeing a real-world example of this, consider the following filmstrip comparison.
Post-It is a Cloud.typography customer, and the entire page stays blank until
the Cloud.typography CSS file makes it onto the device. Grid By Example, on the
other hand, uses Google Fonts (without any font-display
configuration), which
allows the browser to begin rendering the page in the absence of the webfonts.
N.B. The two test cases are very, very different sites, so I’m not looking to directly compare any timings—that would be unfair—I merely want to highlight the phenomenon.
On a 3G connection, Post-It’s Start Render is 16.8s. Removing Cloud.typography entirely improves that time by over 48%, bringing it down to 8.7s.
All the above issues combined mean that, unfortunately, Cloud.typography is slow by default. While I am a huge fan of the foundry, and some of the stunning typefaces they have produced, I cannot in good conscience recommend Cloud.typography as a credible font provider while this is their implementation. It is actively harmful for performance.
The solutions I implemented for my client were mitigations at best—the problems still exist in Cloud.typography, but I was able to do a handful of things to take the edge off of how the problems were manifesting themselves.
First up, I wanted to unblock the JS execution: there was no need to hold back
the main app.js
bundles for the sake of the fonts. The fix? Simple: swap their
<link rel="stylesheet" />
and <script>
lines around in their HTML:
Before:
After:
Yes. As simple as that. Note now that entry (3) executes before the
fonts.[client].com
file has even begun downloading.
My second tactic was to pay the connection cost for fonts.[client].com
up-front by simply preconnecting to that origin. Early in the <head>
:
Note entry (11) in which we’re dealing with a whopping 397ms of connection overhead off of the Critical Path.
These two small changes led to a 300ms improvement in Start Render and a staggering 1,504ms improvement in TTI at the 50th percentile.
N.B. Remember, neither of these changes are solving any of the issues inherently present in Cloud.typography. All I’m doing here is mitigating the issues as best I can on the client’s end.
I help companies find and fix site-speed issues. Performance audits, training, consultancy, and more.
We’re over 1,300 words in and I haven’t even explicitly addressed the crux of
the issue: Cloud.typography doesn’t give us font performance issues, it gives
us CSS performance issues. Because the fonts are Base64 encoded, there are
no fonts. This means that no amount of font-display
, Font Loading API, Font
Face Observer, etc. are going to help us. How do we solve this problem at the
source? I got in touch with Cloud.typography.
Honestly, I started off somewhat confused. With uncacheable, cross-origin redirects on the Critical Path and Base64 encoded fonts, this seems like a very slow way of delivering assets. So, naturally, I wanted to check that we weren’t doing anything wrong. I also wanted to understand the end to end workings of the solution more thoroughly so I’d be better poised to make recommendations.
I got the details of a member of the team at Hoefler&Co and asked if I could send them some questions pertaining to their cloud service. If you’re interested, you can read the unedited email in full.
The response I got shed some light on a few things. To summarise:
Location
of the redirect depends heavily on the User Agent making the
request, so you can’t circumvent the trip to Cloud.typography.I’m incredibly grateful for the person’s time and patience—I just popped on their radar out of nowhere and had quite a barrage of questions and, I’ll admit, critique for them. Their replies were insightful and timely.
However, here’s where I end up a little disheartened: despite clearly outlining the tangible impact that Cloud.typography has on performance, there was no interest in looking at ways to remedy the problems. There was no appetite for providing or even documenting the alternative (i.e. not replacement—the current method would remain fully functional and valid) non-blocking loading strategy.
I came up with a proof-of-concept alternative loading strategy that didn’t block
rendering, instead opting to load the assets asynchronously in exchange for
a flash of fallback text (FOFT), akin to implementing font-display: swap;
,
I was told Customers overwhelmingly prefer to not have their pages load
sans-fonts and then “pop” into place with the correct fonts…
which, of
course, I, every async font loading strategy, and the entire font-display
/Font
Loading spec disagree with.
I ascertained that approximately 13,100 sites in the HTTP Archive currently use Hoefler&Co’s Cloud.typography service. That’s approximately $15.5m a year worth of customers currently bound to a slow-by-design, slow-by-default webfont solution that we could have helped out!
It’s a real shame, as the solution was trivial and I’d done all the legwork, but that’s life.
To reiterate: we don’t have a webfont problem, we have a CSS problem. With this in mind, the solution to the problem becomes devilishly simple: we just need to lazy-load our CSS. By lazy-loading our font stylesheet, we move it off of the Critical Path and unblock rendering.
Since the advent of Critical CSS, lazy-loading stylesheets has become more and more commonplace, with Filament Group’s latest method being by far the simplest and the most widely supported:
The magic lies in the second line. By setting a non-matching media type, the
browser naturally loads the stylesheet with a low priority off of the Critical
Path. Once the file has loaded, the inline onload
event handler swaps to
a matching media type, and this change then applies the stylesheet to the
document, swapping the fonts in.
The one caveat to this method is that we do now have a flash of fallback text
(FOFT). This method of loading the stylesheet effectively synthesises
font-display: swap;
, immediately displaying a fallback typeface and replacing
it with our chosen webfonts as soon as they become available. This means it’s
going to be vital that you design a very robust, accurate fallback style for
users to experience while we’re waiting for the proper fonts to arrive.
Thankfully, Monica has made that a lot simpler
by giving us Font Style Matcher.
What makes me particularly fond of this method is that we’ve gone from arguably the slowest possible method of loading our assets to what might be the least intrusive. A complete inversion of the paradigm.
Here’s a before and after over a 3G connection. The original implementation is on the left; my fixed version is on the right:
Yes, we do have a FOFT, but we are getting text in front of the reader much, much sooner—this is an enormous improvement.
I was genuinely disheartened by Cloud.typography’s indifference to the problems, especially considering the ‘fix’ would only require an update to the documentation. They wouldn’t have had to make any platform or infrastructure changes on their side. Further, they would still have been able to retain the same levels of control and fully track font usage, but completely asynchronously. It would have also been a great press-piece for them, announcing the release of a much faster method of including the same beautiful typefaces.
That said, if you are a Cloud.typography customer, don’t panic. I would recommend exploring the asynchronous method I’ve outlined above. Implementation itself should be trivial, but you will need to invest some time in designing a suitable fallback style while your CSS is loading.
Specific webfont providers aside, I think this entire situation makes for a fascinating case study of how things can begin to slide if enough small mistakes are allowed to consolidate. For me, it was kinda fun to peel back each layer and see how it impacted the next part of the problem, and I hope my detailing it has taught people a thing or two in the process.
If I was to summarise this entire piece with one takeaway, it’s that CSS performance is absolutely critical, and with the current trend of overlooking and dismissing CSS as its own discipline, cases like this are becoming all too common.
CSS matters. A lot.
N.B. All code can now be licensed under the permissive MIT license. Read more about licensing CSS Wizardry code samples…
Harry Roberts is an independent consultant web performance engineer. He helps companies of all shapes and sizes find and fix site speed issues.
Hi there, I’m Harry Roberts. I am an award-winning Consultant Web Performance Engineer, designer, developer, writer, and speaker from the UK. I write, Tweet, speak, and share code about measuring and improving site-speed. You should hire me.
You can now find me on Mastodon.
I help teams achieve class-leading web performance, providing consultancy, guidance, and hands-on expertise.
I specialise in tackling complex, large-scale projects where speed, scalability, and reliability are critical to success.