Zenfonts

github.com/zengabor/zenfonts

Web Fonts Are Beautiful. Once They Are Loaded.

Isn’t it frustrating when text is already there, yet you cannot read it because the fonts are not loaded yet? And that ugly flickering in IE & Edge? They just have no taste!

Take control over font loading. Just the way you want. Zenfonts can help.

759 bytes of vanilla JavaScript. Works everywhere.

View Zenfonts on GitHub

About

Zenfonts is a tiny JavaScript helper to (pre)load web fonts. It isn’t replacing the way of loading web fonts. Instead, Zenfonts is there to help you when the fonts are not loaded in time.

If you are really serious about performance & a smooth experience consider not using web fonts at all. Most platforms have a handful of beautiful fonts already installed which don’t have any of the drawbacks of web fonts. However, if you decide to use web fonts then Zenfonts can be a big help for you:

Of course, loading web fonts is not a problem in most cases. The network is fast enough, the font files are cached after the first load, and the majority of the browser population (Chrome desktop, Chrome for Android, Firefox, Opera) handles font-loading already quite smart, avoiding both the initial flickering and reflow and the “long invisible text” problem by falling back to the next available font after 3 seconds. Other browsers, on the other hand, most prominently iPhones, iPads and old Android browsers hide the text too long if the loading process is slow. Think of Zenfonts as a safety valve. It saves your site if everything else fails.

In the long run Zenfonts will be obsolete once the wonderful css-font-rendering becomes available in the majority of the browser population. At the moment there isn’t a single browser supporting it, so Zenfonts may be quite useful.

Install

Since web fonts are already in the critical path, you don’t want zenfonts to be another blocking request. Instead, inline the minimized version into the page (you can automate this with CodeKit, grunt, etc.). The best place is before the </head> closing tag. Then call it as many times as required, with the desired fonts and parameters. For example:

...
    <script>this.zenfonts=function(e){"use strict";var t="serif",n=e.documentElement,a=function s(e,t){return function(n){if(n||e.offsetWidth!==t){var a=e.parentNode;return a&&a.removeChild(e),!0}}},i=function o(e){e&&(n.className=n.className.replace(new RegExp("(^|\\s)*"+e+"(\\s|$)*","g")," "))};return function r(s,o){if(s instanceof Array||(s=[s]),o=o||{},!e.body)return setTimeout(function(){r(s,o)},1);for(var f=[],l=s.length;l--;){var c=s[l];"string"==typeof c&&(c={family:c});var u=c.family,m=e.createElement("div");m.style.cssText="position:absolute;top:-999px;left:-9999px;visibility:hidden;white-space:nowrap;font-size:20em;font-family:"+t+";"+c.style||"",m.appendChild(e.createTextNode("// Zenfonts([{}]);")),e.body.appendChild(m);var p=a(m,m.offsetWidth);m.style.fontFamily="'"+u+"',"+t,p()||f.push(p)}var d=o.fallbackClass,v=function b(){i(d),o.onSuccess&&o.onSuccess()};if(0===f.length)return v();var h=o.loadingClass;h&&(n.className+=" "+h);var y=o.fallback||2e3;if(d){var g=setTimeout(function x(){i(h),n.className+=" "+d},y),w=v;v=function(){clearTimeout(g),w()}}var T=(new Date).getTime()+Math.max(y,o.swap||9999);!function N(e){setTimeout(function(){var t=(new Date).getTime()>=T;t&&o.onSwap&&o.onSwap();for(var n=f.length;n--;)f[n](t)&&f.splice(n,1);0===f.length?(t||v(),i(h)):N(1.5*e)},e)}(9)}}(document);</script>
    <script>
        zenfonts("Beautiful Font", {fallbackClass: "fallback-1", fallback: 1000, swap: 1000})
        zenfonts(["Font2", "Font3"], {loadingClass: "load", fallbackClass: "fallback-2"})
    </script>
</head>

Tip: You can actually copy and use the above code to install Zenfonts. The first <script> line contains the current version of Zenfonts. Just make sure you copy the entire line.

You can also download Zenfonts, or use npm:

npm install zenfonts

Of course, your web fonts must be already specified by @font-face or must be loaded in some other way (like by a JavaScript font loader). It doesn’t matter how; for Zenfonts it’s all transparent.

How it works

Tip: Feel free to skip this dry theory section & go straight to Examples.

zenfonts(fonts, options) returns immediately after calling but it stays in the background and periodically checks whether the specified fonts are loaded. Zenfonts acts only when the four basic events occur (see below), performing what you specified. (Note that this background checking gets less and less frequent with time. Finally, by default after 10 seconds, Zenfonts gives up and removes itself completely.)

Important: The font-families you reference in the fonts parameter must be already specified, typically in your CSS as @font-face declarations.

The two parameters detailed:

1. fonts is either an array or a single one. Every passed font is either specified as a string or as an object with optional style attributes. Some examples:

2. options is an object with attributes that are all optional:

Here is a complex example:

var saunaFonts = ["Sauna", {family:"Sauna", style:"font-style:italic"}, {family:"Sauna", style:"font-weight:bold"}]
var options = { 
    loadingClass: "loading-ie",
    fallbackClass: "fallback-headers fallback-bodytext", 
    fallback: 2500, swap: 2500,
    onSuccess: setSaunaCookies, 
    onSwap: function () { zenfonts(saunaFonts) } 
}
zenfonts(saunaFonts, options)

Note that both loadingClass and fallbackClass can be a list of classes, separated by spaces, e.g., "loading-headers loading-bodytext". With the help of these two parameters you can design in detail how to handle the loading phase and how to fall back to other fonts if the specified fonts could not be loaded in time.

Here are the four basic events, and what the actions Zenfonts performes at each:

START: You call zenfonts()

  1. loadingClass is applied on the root <html> tag (if you provided it).
  2. A hidden DIV is created for each font you specified, forcing the browser to load those fonts within.

SUCCESS: The specified fonts are all loaded

  1. loadingClass is removed (if you provided it).
  2. fallbackClass is removed (if it was applied earlier).
  3. onSuccess it is executed (if you provided it).

FALLBACK: fonts not loaded until timeout (2 seconds by default)

  1. fallbackClass is applied on the root <html> tag (if you provided it).
  2. loadingClass is removed (if you provided it).

SWAP: Zenfonts gives up (10 seconds by default)

  1. onSwap is executed (if you provided it).
  2. loadingClass is removed (if you provided it).
  3. fallbackClass is not removed.
  4. Zenfonts stops the monitoring & quits.

Examples

Here are a few generic ways of using Zenfonts. You can combine and extend these techniques for your specific needs.

Tip: Don’t forget to also look at the source of the demo page.

1. Group fonts

To minimize reflows (invisible state and/or swap) group fonts that go together, like headlines and body, or the various fonts from the single typeface:

zenfonts(
    ["Typeface1", {family: "Typeface1", style: "font-style: italic"}, {family: "Typeface1", style: "font-weight: bold"}],
    { fallbackClass: "fallback-all" }
)

2. Quick fallback for critical content

Pay special attention to the fonts used for the most prominent content, like headlines and body text, because reflows can ruin the first experience of your site, and users want to read the content as early as possible. Group these fonts together, and set up an early swap. For example:

zenfonts(["TitleFont", "BodyFont"], {fallbackClass: "fallback", fallback: 1000, swap: 1000})

3. Fallback first for critical content

If content is even more critical then you start with a local font. At the same time preload the web font in the background and remember in a cookie when it’s done. When the user navigates to the next page the font is already cached (you can assume this from the existing cookie) so you can switch to your beautiful content font. Even if the cookie lies Zenfonts still controls the loading and will switch quickly to the fallback font.

Here’s a complete example:

var isLizaProbablyCached = document.cookie.indexOf("liza=cached") > -1
if (isLizaProbablyCached) {
    zenfonts("Liza", { fallbackClass: "fallback-liza", fallback: 1000, swap: 1000 })
} else {
    document.documentElement.className += " fallback-liza"
    zenfonts("Liza", { 
        onSuccess: function () {
            document.cookie="liza=cached; expires=Wed, 1 Jan 2020 00:00:00 UTC; path=/"
        }
    })
}

4. Preload the fonts you’ll need later

If you know that a specific font will be needed later (e.g., in a dialog box or on a next page) you can use Zenfonts to preload it. There is no harm done even if the font is already in the browser cache because then Zenfonts simply quits. Make sure you don’t specify a fallbackClass when doing preloads.

A few examples:

zenfonts("Liza")
zenfonts(["Dolly", "Fakir", "Bello"])
zenfonts([
    "Sauna",
    {family:"Sauna", style:"font-weight: 300"},
    {family:"Sauna", style:"font-weight: bold; font-style: italic"}
], { 
    onSuccess: function () {
        document.cookie = "sauna=cached; expires=Wed, 1 Jan 2020 00:00:00 UTC"
    }
})

5. Fonts not loaded in time? Fall back & keep loading.

You can keep loading the fonts after a swap so that they get cached by the browser, and will be available at the next page load. Just call Zenfonts in onSwap:

var myFonts = ["TitleFont", "BodyFont", {family: "BodyFont", style: "font-style: italic"}]
zenfonts(myFonts, {
    fallbackClass: "fallback", fallback: 1000, swap: 1000, 
    onSwap: function () { zenfonts(myFonts) } 
})

6. Check out the source code of this page

The @font-face definitions are at the beginning of the references styles.css file.

Then the CSS rules related to the web fonts are inlined in a <style> block for easier access. After the normal definition of the font-family there is always a fallback with the right prefix: .fallback, .fallback-logotype, and .fallback-mono. (Which could be done more elegantly with a CSS preprocessor, like Sass.)

Right after comes the minimized version of Zenfonts inlined, and then three calls of zenfonts():

  1. For the body text with a low fallback & equal swap timeout, so content won’t get blocked long. There is also a loading class to avoid FOUT on IE & Edge.
  2. I give a lot more time for the logotype as it doesn’t cause reflows, and I keep loading it for another 30 seconds in case of a swap.
  3. Finally, the font for the code examples fall back after 1 second but then they keep loading until the default 10 seconds timeout and they appear as soon as they become available (the code uses preformatted line breakes so there can’t be significant reflows).

This is an example of finding the best strategy for each type of content/font combinations.

To experience how this page loads on a slow network:

  1. Disable the browser’s cache or clear it. (Otherwise fonts may be loaded from there.)
  2. Load the page on a slow or emulated network. (E.g., turn on network throttling in Chrome DevTools, or use a slow mobile network.)

License

Public Domain. You can do with it whatever you want and I am not responsible for anything.

About Me

Gabor LenardMy name is Gabor Lenard. You can find me on Twitter as @zengabor. I am a product designer & developer, currently building zenvite.com. It never ceases to amaze me how people share their knowledge and what they make. Their work empowers us all. This here, however humble, is my way of contributing to this community & the wonderful continuum we call the world wide web.

Other projects by me: