Zach’s ugly mug (his face) Zach Leatherman

Web Font Anti-pattern: Data URIs

March 11, 2016 On Twitter (archived)

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">
	<meta charset="utf-8">
	@font-face {
		font-family: Open Sans;
		src: url("data:font/woff;charset=utf-8;base64,…") format("woff");
		font-weight: 400;
		font-style: normal;

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, 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 Loading Roman Data URI and Italic Data URI and Bold Data URI and Bold Italic Data URI
Initial Render 573ms
Roman Loaded 2.12s 1.01s
Italic Loaded 2.12s 2.05s
Bold Loaded 2.20s 2.11s
Bold Italic Loaded N/A N/A N/A N/A 2.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 FOFT Critical Roman Data URI
Initial Render 570ms
Stage 1 Render
(Critical Roman)
967ms 580ms
Stage 2 Render
(Roman, Italic, Bold, Bold Italic)
2.70s 2.42s

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 FOFT Critical Roman Data URI
Initial Render 309ms 293ms
Critical Roman Loaded 479ms 435ms
Roman Loaded 479ms 435ms
Italic Loaded 479ms 435ms
Bold Loaded 479ms 435ms
Bold Italic Loaded 479ms 435ms

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:font/woff2;charset=utf-8;base64,…") format("woff2"), url( /path/to/webfont.woff ) format( "woff" );

< Newer
Trying out a new Font Stack
Older >
A Versatile 2KB of JS

Zach Leatherman IndieWeb Avatar for a builder for the web and the creator/maintainer of IndieWeb Avatar for https://www.11ty.devEleventy (11ty), an award-winning open source site generator. At one point he became entirely too fixated on web fonts. He has given 81 talks in nine different countries at events like Beyond Tellerrand, Smashing Conference, Jamstack Conf, CSSConf, and The White House. Formerly part of CloudCannon, Netlify, Filament Group, NEJS CONF, and NebraskaJS. Learn more about Zach »

1 Repost

IndieWeb Avatar for
  1. Wim Leers Disqus

    06 Jun 2016
    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 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 small2. convert it to WOFF (88% browser support currently)3. embed WOFF in CSS as data: URI4. ensure you have as little CSS as possible, and only a single fileThe 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.)
    1. zachleat Disqus

      09 Jun 2016
      I hope that the article didn’t come across as calling you out—I have since learned that 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.
      1. Wim Leers Disqus

        11 Jun 2016
        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!
  2. mobiles Disqus

    17 Jul 2016
    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?
    1. zachleat Disqus

      21 Jul 2016
      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:
  3. Stephen James Disqus

    22 Jul 2016
    Great article, cleared up a lot and saved me having to get out the the lab coat, thanks again! :)
  4. etler Disqus

    02 Sep 2016
    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!
    1. zachleat Disqus

      02 Sep 2016
      1. etler Disqus

        02 Sep 2016
        Thanks for the response, I did read those tweets, I just couldn't find the right tools to do all that.
        1. zachleat Disqus

          02 Sep 2016
          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
          1. etler Disqus

            02 Sep 2016
            Thanks a ton! Awesome stuff
  5. David Alimian Disqus

    02 Jun 2017
    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)
    1. zachleat Disqus

      08 Jun 2017
      Not sure what you’re saying here. Can you clarify?
      1. David Alimian Disqus

        10 Jun 2017
        sorry, I meant woff2 enjoys lot more browser support now than just not too long ago. . 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?
        1. zachleat Disqus

          12 Jun 2017
          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:
  6. Jonas Disqus

    31 Jul 2017
    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!
    1. zachleat Disqus

      09 Aug 2017
      Hey Jonas—thanks! Did you see this comment? may also enjoy this article from Roel Nieskens:
Shamelessly plug your related post

These are webmentions via the IndieWeb and

Sharing on social media?

This is what will show up when you share this post on Social Media:

How did you do this? I automated my Open Graph images. (Peer behind the curtain at the test page)