- phantomunderlines. isn't thisamaaaaaazing.
- i love waiting for 8 seconds and seeing this.
- look at it. srsly.looooookat it.
I spent a week traveling around Taiwan, on my awesome free roaming 2G data plan, and friends, we need to talk about your web fonts. Also cats. They really love cats there. Anyway, the thing about 2G is that I fully understand that it will take me 10 seconds to load a page. What sucks is the fresh rage of the following 4 seconds where instead of content I get phantom underlines, waiting for a slightly-different-sans-serif to download.
Listen: it doesn’t have to be this way. You can lazy load your font. It’s 4 lines of JavaScript. 7 if you’re being ambitious.
Why should you care
I’ve been brainwashed to really care about first paint performance (thanks Chrome Dev Rel 😘), and I’ve become a big fan of the “do less & be lazy” approach to building things. What this means is that if something is not on your critical path, it probably doesn’t need to be the first thing you paint on a page.
Now think about fonts: is the critical path showing text, or styling it? I’d argue that unless your app is in the 1% it’s-all-a-magical-visual-experience bucket (in which case this post is not for you), it’s probably trying to communicate some content, and ugly content (that you prettify after) is better than no content.
(Real talk: if you don’t think rendering text is a critical path, you’re whack and we need to have a chat.)
There are two things you can run into when loading a web font:
FOIC (“flash of invisible content”) – when your browser sees that you’re trying to use a font it doesn’t have it paints all the text in invisible ink, waits, and when it finally gets the font, it re-paints and re-layouts the text correctly. I hate this with the fire of a thousand suns, because instead of looking at actual content, I’m looking at bullets and underlines and random text you forgot to style. Neat-o.
FOUC (“flash of unstyled content”) – Chrome stops waiting for a web font after 3 seconds (and, recently, after 0 seconds on 2G). What this means is instead of showing you invisible ink, it paints the text in your fallback font. When your web font is finally downloaded, it then re-paints the already displayed text with the new font.
Side note: on iPhones, this timeout doesn’t exist, so you basically only get a FOIC – you wait the entire time to get from “no text” to “all the text”, with no intermediate bail out state.
(Here is the code that I used for these demos, with GPRS and 2G throttling respectively in Chrome. This demo will look super snappy on LTE. Everything is super snappy on LTE.)
Reading material
A lot of people have written about web fonts, and I’m not trying to re-write their
posts. Chrome in particularly has been working a lot on improving this, by
decreasing the web font download timeout to 0s on 2G, and working on the font-display
spec.
Here are some links I like:
- the anatomy of a web font and thedance that a browser does to use a web font
font-display
options, and how it affects how fonts loadfont-display: optional
and why it’s awesome (tl; dr: if you can’t do it fast, don’t do it at all)- minimizing font downloads by limiting the range of characters you’re loading
- why we should care about web fonts and how to minimize FOIT using JavaScript and a library called Font Face Observer
- voltron solution combining FontFaceObserver, async loading a font bundle and web storage
Lazy loading a font
Personally, I would use font-display: optional
everywhere, but that doesn’t really work anywhere yet.
In the meantime, here are 2 super simple ways to lazy load a web font.
Again, I don’t really mind having a FOUC, since it feels like progressive enhancement to me:
display the content as soon as you can, and progressively style it after.
<head><style>body{font-family:'Arima Madurai',sans-serif;}</style></head><body>...</body><script>// Do this only after we've displayed the initial text.document.body.onload=function(){varurl='https://fonts.googleapis.com/css?family=Arima+Madurai:300,400,500';loadFont(url);// hold tight, i tell you below.}</script>
There’s basically two ways in which you can implement that loadFont
:
Load the stylesheet (synchronous)
This is the simplest way and works great for a simple page. But! Since loading/parsing a stylesheet blocks rendering, this doesn’t play nicely if you’re loading a bunch of other modules after the document has loaded, since they will be delayed. [demo]
varlink=document.createElement('link');link.rel='stylesheet';link.href=url;document.head.appendChild(link);
XHR the stylesheet (asynchronous)
If you care about synchronicity (and tbh you probably should), you can do an async XMLHttpRequest and create a style node with the result. [demo]
varxhr=newXMLHttpRequest();xhr.open('GET',url,true);xhr.onreadystatechange=function(){if(xhr.readyState==4&&xhr.status==200){varstyle=document.createElement('style');style.innerHTML=xhr.responseText;document.head.appendChild(style);}};xhr.send();
For bonus points, you can take this one step further and
rather than creating an inline <style>
,
append a <link>
like in the previous method, since the browser cache is already
primed. If you trust your browser cache. I trust no one.
This isn’t perfect. On your shiny LTE connection, this always flashes from unstyled to styled text, whereas the first demo (that didn’t try to deal with web fonts at all), doesn’t. Ideally you would combine this with some local storage magic so that repeat views can just load the font from the cache and prevent subsequent FOUC.
TL; DR
Web fonts are okay. They make your blog prettier. They’re also slow and kind of an annoying experience, but if you need to use them, use them. Just remember that it’s also your responsibility to make your site load super fast, and if you don’t, it’s totes fair game for people (me) to whine about it on Twitter.
(🍹 to Paul Lewis who had to sit through all my questions and explain basic browser things to me. Again.)