Zach’s ugly mug (his face)

Zach Leatherman

Web Font Anti-pattern: Data URIs

11 Mar 2016 Zach Leatherman 10 min read

After I posted my Critical Web Fonts article on Twitter, I had an interesting conversation with a developer named Wim Leers about embedding Web Fonts as Data URIs.

His suggestion was to embed the font directly in a style block on the server rendered markup, something like:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
@font-face {
font-family: Open Sans;
src: url("data:application/x-font-woff;charset=utf-8;base64,...") format("woff");
font-weight: 400;
font-style: normal;
}
</style>
</head>
</html>

This approach should not to be confused with the asynchronous loadCSS Data URI approach documented (but deprecated) on the Filament Group blog.

I’ve seen a similar variant of this Data URI approach used by Alibaba.com, although their approach used an external stylesheet rather than an inline style element. I talked a little bit about it at Velocity last year.

I consider this approach to be an anti-pattern for normal font loading scenarios for a few reasons:

  1. It puts a large Data URI in the critical path. Remember that CSS blocks rendering. The goal here is to avoid a Flash of Invisible Text (FOIT) and minimize our Flash of Unstyled Text (FOUT). It obviously isn’t a good tradeoff to delay the entire page render to avoid FOIT and FOUT. Since 42% of web sites load more than 40KB of web font page weight, many sites would need to put 40KB of Data URIs in their critical path, far exceeding the recommended 14KB window for critical content.
  2. The font format you embed is probably not optimal. If you embed a Data URI, you’ll probably embed the WOFF format to give you ubiquity (better browser support) even though the WOFF2 format usually has about a 30% smaller footprint. Embedding a single format removes the benefit of automatic format selection that a typical comma separated src attribute provides. You aren’t required to list only one src here, but for example let’s say you embed a WOFF2 format Data URI and list the WOFF format as an alternate external url in the src attribute. There are still quite a few modern browsers that don’t support WOFF2 and they would load that big Data URI and still have to resort to using a fallback format URL. (See Appendix 1, Data URI and Fallback src below.)
  3. Ability to cache fonts suffers. This approach worsens with repeat views because the Data URI is tightly coupled to the markup and will not be cached (unless the user visits the same destination twice).
  4. The other drawback Bram Stein mentions in his latest presentation (and has a great waterfall showing it, too): if you have multiple web fonts, making them all Data URIs forces them to be loaded sequentially (bad) instead of in parallel (good).

Update: Wim Leers has since informed me that the approach he was proposing was not inlined critical CSS, but rather a blocking CSS stylesheet, a la the approach used by Alibaba and Medium. The above drawbacks still stand, save for #3. What’s more, this approach probably exacerbates drawback #1, given the performance gains we already know exist when using Critical CSS.

For those reasons, this method is considered to be an anti-pattern and should not be utilized on a production site. It may seem superficially beneficial, but it’s actually bad for performance.

But just for the sake of argument, let’s put it into action and see how it affects the fonts on my web site:

(Times generated using Chrome Canary’s Developer Tools Network Throttling in Regular 3G mode)

Default Font LoadingRoman Data URIand Italic Data URIand Bold Data URIand Bold Italic Data URI
Initial Render573ms
 
54KB HTML
953ms
(+66%)
95.7KB HTML
1.27s
(+121%)
133KB HTML
1.94s
(+238%)
175KB HTML
2.30s
(+301%)
212KB HTML
Roman Loaded2.12s1.01s
(-52%)
1.53s
(-28%)
2.03s
(-4%)
2.38s
(+12%)
Italic Loaded2.12s2.05s
(-3%)
1.53s
(-28%)
2.03s
(-4%)
2.38s
(+12%)
Bold Loaded2.20s2.11s
(-4%)
2.16s
(-2%)
2.03s
(-8%)
2.38s
(+8%)
Bold Italic LoadedN/AN/AN/AN/A2.38s

Interestingly enough, if you only inline the Roman version and you’re willing to sacrifice almost 400ms of extra time for initial render (wow, that is a big sacrifice), you can cut a whole second off the Roman web font rendering time. Initial render suffers even worse with a second inlined font, and if you inline more than two fonts (ignoring web font SPOF concerns) performance-wise you’re better off doing nothing (compare the 1st and 4th columns). Also note that I didn’t test repeat views in the above table because the data was conclusively negative without it.

But wait…

The discussion started with embedding the entire web font, but what if we apply this idea to the Critical FOFT approach? What if we only inline the critical subset font? My hunch is that the subset 11KB WOFF font file (much smaller than the 40KB average) is probably still too large to put into the critical path, but let’s test it out.

(Times generated using Chrome Canary’s Developer Tools Network Throttling in Regular 3G mode)

Update March 17, 2016: Per a discussion with @pixelambacht, we were able to optimize the WOFF font file to only 5KB! I’ve updated the results below. This shaved about 5-10 more milliseconds off in the tests that I ran, so depending on your design preferences you should make your own choices about whether those optimizations are worth it.

Empty Cache Visit
Critical FOFTCritical Roman Data URI
Initial Render570ms
 
58.2KB HTML
580ms
(+1.7%)
65.4KB HTML
Stage 1 Render
(Critical Roman)
967ms580ms
(-40%)
Stage 2 Render
(Roman, Italic, Bold, Bold Italic)
2.70s2.42s
(-10%)

Wow! No visible FOUT! This is a huge deal. The critical web font is available on first render with an empty cache on 3G. This is great! The only problem here, of course, is that for repeat views the Data URI is still inlined on the page. Let’s test that out:

Repeat Views
Critical FOFTCritical Roman Data URI
Initial Render309ms293ms
(-5.1%)
Critical Roman Loaded479ms435ms
(-12%)
Roman Loaded479ms435ms
(-9%)
Italic Loaded479ms435ms
(-9%)
Bold Loaded479ms435ms
(-9%)
Bold Italic Loaded479ms435ms
(-9%)

Times look marginally better here too. Huh. I think I’m gonna roll with this approach on my website and see how it plays out live. Thanks for the discussion Wim! I guess I learned here that just because something is an anti-pattern doesn’t mean you should throw the baby out with the bath water. You might get some benefit from using a piece of the approach. Data URI Critical FOFT!

Appendix 1, Data URI and Fallback src

@font-face {
/* In many browsers it loads the giant Data URI but isn’t able to use it */
src: url("data:application/font-woff2;charset=utf-8;base64,...") format("woff2"), url( /path/to/webfont.woff ) format( "woff" );
}

Retweet to share this post

17 Comments

➡ Load Disqus to Leave a Comment ⬅

Nice article, although it starts with a premise that isn't what I actually said/meant, as you explain later in the blog post. You could've also seen that it's not what I meant by looking at the source code of wimleers.com of course ;)

The 4 downsides you list are indeed true of the approach you described (but it's not the one I use). With the approach I use, you say only downsides 1, 2 and 4 remain.

Downside 1 is definitely true, but I accept that trade-off. I'd rather not have to deal with the case of the font not being loaded yet. Plus, inlining critical CSS is tricky to automate, especially in a modular site where only the necessary CSS is loaded. I don't want to have to update my inline critical CSS manually every time I update Drupal modules.

Downside 2 is also true, but … what I did 4 years ago still works, and still works fast. WOFF2 still has only 54% browser support. I'll just stick with WOFF for now. It also means I'm not embedding the same font in two formats in my CSS file. So, once again a conscious trade-off.

Downside 4: I think it's better to simply completely avoid ever loading multiple fonts. Stick to a single font. Yes, my site will perhaps look less special, but I think that's okay. I use the font where it matters.

But you forget to list the downside of the approaches you do recommend: complexity & maintainability. Having fonts in many formats means updating those fonts, and keeping them in sync in each format. WOFF and WOFF2 didn't exist yet when I applied my technique. You have to deal with FOUT.

I think the technique strikes a nice balance:
1. use a single font, with subsetting performed — this makes it very small
2. convert it to WOFF (88% browser support currently)
3. embed WOFF in CSS as data: URI
4. ensure you have as little CSS as possible, and only a single file

The end result is simple, prevents FOUT altogether, has a single critical resource in the critical path (besides the HTML), and has subsequent sub-second cold load times in Europe, sub-2-second load times in USA, sub-500ms warm load times everywhere.

So, I think this is a fine solution for non-high-end sites.

As always: needs differ, compromises need to be made. We can't _always_ look for the absolute best solution. And the absolute best solution is itself usually a compromise, at least in the world of the WWW :)

(Remember, this is my personal site, not a multi-million-dollar-revenue-generating site.)

I hope that the article didn’t come across as calling you out—I have since learned that medium.com takes a similar approach (except they use quite a few embedded Data URIs). For me, the approach I’ve documented above has the best tradeoffs dealing with unreliable and slow network connections, while only sacrificing a minimal amount of initial rendering for a single web font.

If I only used one web font (which is kind of uncommon on the projects I’ve worked on that used them), I may embed the whole thing as you’ve suggested.

It felt that way at first, but it's okay — I know you didn't intend it that way :)

I think it makes sense that people choose different trade-offs for different projects. That's mostly why I left a comment: to clarify that I chose this while I very well knew this wasn't the optimal approach, but it was the simplest approach that solved the problem I saw, back in 2012.

Thanks for writing about this, and for doing all the research you do in determining what is the optimal approach!

Great article (and the one linked to by Ansel on Smashing Mag) but how to inline a font (ttf?). Is there some convertor online to make it into something to add to a CSS?

Anything that creates a Data URI will do it.

I think the easiest thing is probably to use the Font Squirrel WebFont Generator with the “Base64 Encode” option checked in the Expert mode: https://www.fontsquirrel.co...

Great article, cleared up a lot and saved me having to get out the the lab coat, thanks again! :)

I was looking into doing this and wanted to see if anyone else had tried and your writeup on it answered all my questions and went further in depth than I was planning on going so you saved me a ton of trouble! The idea of inlining a stripped down version of the font is great.

But how did you get a font down to 5KB? I'm trying with Lato, and with fontsquirrel I can only get it down to about 14KB (woff2), and with FontForge I got it down to 10KB (woff2), but I can't figure out how to get any further. What tools and steps did you use to get it further down?

Also, you might want to consider renaming the title or putting an edit at the top since the conclusion is that it actually isn't an anti pattern!

Thanks for the response, I did read those tweets, I just couldn't find the right tools to do all that.

I believe the actual command used was:

pyftsubset lato-regular-alphabetsubset.ttf --glyphs=A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,space --verbose --no-hinting --desubroutinize --drop-tables+=GSUB --flavor=woff

Thanks a ton! Awesome stuff

u might want to reconsider classifying this as anti pattern cause woff2 enjoys lot more support now since time of this writing (abt 80-90% in US)

Not sure what you’re saying here. Can you clarify?

sorry, I meant woff2 enjoys lot more browser support now than just not too long ago. http://caniuse.com/woff2/em... . with safari 10 on ios now supporting woff2 my rough estimate is 80-90% of web users in united states are on a browser that supports woff2. so maybe in some case now it would be worth having the woff2 as data uri?

Yeah, in some cases that might make sense!

Depends on where in the resource hierarchy you want to place your web fonts. I think the ubiquity of a WOFF still outweighs the marginal performance benefit here, but your priorities may differ!

To be clear though, I probably wouldn’t use this method. A full list of methods and opinions can be found here: https://www.zachleat.com/we...

Hey Zach, great article :)!
I'm curious as per the March 17th update. How did you optimize the font subset to 5KB from 15KB?

Having a similar issue right now because we load a lot of fonts, but I want to go with only the small subset. Currently that's 19KB so it's a no-go if I can't reduce to at least ~10KB :)

Thanks a lot!

Hey Jonas—thanks! Did you see this comment? https://www.zachleat.com/we...

You may also enjoy this article from Roel Nieskens: https://pixelambacht.nl/201...