Web Font Anti-pattern: Data URIs
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.
@zachleat @MarcDrummond Why not just embed as a data URI in the critical CSS? Cached, no FOUT. Example: https://t.co/tqQ03G3dhf.
— Wim Leers (@wimleers) January 6, 2016
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: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:
- 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.
- 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 onesrc
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 thesrc
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.) - 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).
- 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 |
953ms (+66%) 95.7KB HTML |
1.27s (+121%) 133KB HTML |
1.94s (+238%) 175KB HTML |
2.30s (+301%) 212KB HTML |
Roman Loaded | 2.12s | 1.01s (-52%) |
1.53s (-28%) |
2.03s (-4%) |
2.38s (+12%) |
Italic Loaded | 2.12s | 2.05s (-3%) |
1.53s (-28%) |
2.03s (-4%) |
2.38s (+12%) |
Bold Loaded | 2.20s | 2.11s (-4%) |
2.16s (-2%) |
2.03s (-8%) |
2.38s (+8%) |
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.
Critical FOFT | Critical Roman Data URI | |
---|---|---|
Initial Render | 570ms |
580ms (+1.7%) 65.4KB HTML |
Stage 1 Render (Critical Roman) |
967ms | 580ms (-40%) |
Stage 2 Render (Roman, Italic, Bold, Bold Italic) |
2.70s | 2.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:
Critical FOFT | Critical Roman Data URI | |
---|---|---|
Initial Render | 309ms | 293ms (-5.1%) |
Critical Roman Loaded | 479ms | 435ms (-12%) |
Roman Loaded | 479ms | 435ms (-9%) |
Italic Loaded | 479ms | 435ms (-9%) |
Bold Loaded | 479ms | 435ms (-9%) |
Bold Italic Loaded | 479ms | 435ms (-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:font/woff2;charset=utf-8;base64,…") format("woff2"), url( /path/to/webfont.woff ) format( "woff" );
}
17 Comments
Wim Leers Disqus
06 Jun 2016zachleat Disqus
09 Jun 2016Wim Leers Disqus
11 Jun 2016mobiles Disqus
17 Jul 2016zachleat Disqus
21 Jul 2016Stephen James Disqus
22 Jul 2016etler Disqus
02 Sep 2016zachleat Disqus
02 Sep 2016etler Disqus
02 Sep 2016zachleat Disqus
02 Sep 2016etler Disqus
02 Sep 2016David Alimian Disqus
02 Jun 2017zachleat Disqus
08 Jun 2017David Alimian Disqus
10 Jun 2017zachleat Disqus
12 Jun 2017Jonas Disqus
31 Jul 2017zachleat Disqus
09 Aug 2017