Web font loading detection, without timers

After writing my first post about how to be notified when specific font-families downloaded and rendered on a web page, I kept thinking that this approach wasn’t too efficient. The fact that the time taking to download and render fonts on a web page depends on many different factors like the network latency and processing power of the client device, makes it difficult to choose the right time interval for sampling dimensions of elements. Using too long interval may introduce extra waiting time if the font was loaded just after new timer iteration was started. On the other hand, using too short interval may present processing time overhead when trying to get the dimensions of elements by obtaining layout triggering properties like offsetWidth and offsetHeight. So I kept thinking how you can force the browser to notify you when custom font is loaded without polling the dimensions of “font container” element.

Following event timeline overview show the difference between the events fired in web page when using typekit’s Web Font Loader that uses timeouts, and the events fired when using method that does not use timeouts, as explained in this post. These event timelines are taken from the chrome developer tools.

typekit events timeline (with timeouts)

typekit events timeline (with timeouts)

fontloader event timeline (without timeouts)

fontloader event timeline (without timeouts)

If you just want to use the FontLoader without reading this post you can download it on Github:

  Download on Github

Step I: Searching for an event

The first thing that may come to mind is to add some sort of “resize” event listener to a “tester” element with some text and default font-family (e.g. font-family: serif;). Then, adding new font-family with fallback to the default font-family (e.g.: font-family: CustomFont, serif;) should change the size of the element and fire the “resize” event as soon as the new font is loaded and rendered, assuming of course that the new font has different font metrics.

<div id="tester" style="position:absolute; font-family:serif;">Hello World!</div>
<script>
    var element = document.getElementById("tester");
 
    element.addEventListener("resize", function() {
        console.log("resize fired, font loaded");
    }, false);
 
    element.style.fontFamily = "Georgia, serif";
</script>

Well, the good news is that DOM specification does have the “resize” event. But the bad news is that this event is never fired on HTML elements, it is fired only on window when it is resized. So running the above example in browser won’t log the “font loaded” text. The fun part is that IE9 (and maybe other versions of IE) does fire this event when element size changes. But it does so only when binding the event using the non-standard attachEvent method with “on” prefix and only when adding the new font-family after 0 timeout:

<div id="tester" style="position:absolute; font-family:serif;">Hello World!</div>
<script>
    var element = document.getElementById("tester");
    if (typeof element.attachEvent === "function") {
        element.attachEvent("onresize", function () {
            console.log("onresize fired, font loaded");
        });
    }
    window.setTimeout(function() {
        console.log("add new font-family");
        element.style.fontFamily = "Georgia, serif";
    }, 0);
</script>

console output:

add new font-family 
onresize fired, font loaded

Nevertheless we need to find another DOM event and utilize it in a way that will mimic the “resize” event by being fired on an element when its size have changed. Great! But what is this event?

Taking a look at the event handlers available for DOM elements at first glance may seem that there are no events which could help us to solve our problem. However, there is one event that can help us, and it is the scroll event. According to W3C specification:

“A user agent must dispatch this event when a document view or an element has been scrolled.”

Generally, we have two explicit ways to scroll an element and trigger the “scroll” event. The first way is by using user inputs like mouse or keyboard (e.g.: dragging scrollbars, scrolling the mouse wheel, pressing up and down keys, etc.). And the second way is by changing the scrollTop property of an element using JavaScript. But there is also a third – implicit - way to scroll an element and it is by changing the size of the element or the size of its contents while satisfying a special initial condition. Thus, causing the “scroll” event to be dispatched when element size changes.

Step II: Setting up the environment

For the beginning let’s setup the test environment for our experiments. First, we will create an element called “wrapper” having height of 100 pixels. Inside this element we will place another element called “content” having height of 150 pixels. Our goal is to cause the “scroll” event to be dispatched on the “wrapper” element when its size, or the size of the “content” element, is changed. We will apply overflow: hidden; on the “wrapper” element to clip its contents and hide the scrollbars to disable scrolling by user inputs but allowing it to be scrolled by using JavaScript. Then we will change the height of the “content” element using JavaScript to simulate change in size as a result of downloaded and rendered font.

<div id="wrapper" style="width:100px; height:100px; overflow:hidden;">
    <div id="content" style="height:150px;"></div>
</div>
<script type="text/javascript">
    var wrapper = document.getElementById("wrapper"),
        content = document.getElementById("content");
 
    console.log("initial scrollTop: " + wrapper.scrollTop);
 
    content.style.height = "140px";
    console.log("scrollTop after decreasing content height: " + wrapper.scrollTop);
 
    content.style.height = "160px";
    console.log("scrollTop after increasing content height: " + wrapper.scrollTop);
</script>

Running the above code will output next lines in console:

initial scrollTop: 0
scrollTop after decreasing content height: 0
scrollTop after increasing content height: 0

This example shows that trying to simply change the height of the “content” element will not change the scrollTop property of the “wrapper” element. Actually it’s pretty obvious because changing the height of the content only expands downwards or contracts upwards its bottom edge while its top edge remains at the same place. And of course, if the scrollTop property has not changed then the “scroll” event could not have fired.

Part III: Triggering the scroll event by decreasing content size

Ok, so we did not succeed with our first try. But no worries, we are engineers and engineers aren’t giving up that easy! This time, let’s try to add the special initial condition I was talking about: before setting new height on the “content” element we are going to scroll the “wrapper” element to the bottom. To do this we will set new value for the scrollTop property. The new scrollTop value can be calculated by subtracting clientHeight from scrollHeight: 150 – 100 = 50. Then we will set new height on the “content” element and see what happens.

wrapper.scrollTop = wrapper.scrollHeight - wrapper.clientHeight;
console.log("initial scrollTop: " + wrapper.scrollTop);
 
content.style.height = "140px";
console.log("scrollTop after decreasing content height: " + wrapper.scrollTop);

console output:

initial scrollTop: 50
scrollTop after decreasing content height: 40

Wow! Did you saw that? The scrollTop property has been changed. But not only that, if we would have added the “scroll” event listener to the “wrapper” element before changing content’s height, the event would have fired right after setting the new height:

wrapper.scrollTop = wrapper.scrollHeight - wrapper.clientHeight;
console.log("initial scrollTop: " + wrapper.scrollTop);

wrapper.addEventListener("scroll", function() {
    console.log("wrapper scrolled, scrollTop: " + this.scrollTop);
}, false);

content.style.height = "140px";
console.log("scrollTop after decreasing content height: " + wrapper.scrollTop);

console output:

initial scrollTop: 50
scrollTop after decreasing content height: 40
wrapper scrolled, scrollTop: 40

This is Magic! But in fact, what happens is that decreasing content height while it is scrolled to the bottom pulls the content down to keep its bottom edge glued to the bottom of the wrapper forcing its scrollTop property to be changed and the “scroll” event to be fired.

Note: This “magic” doesn’t work with IE, at least not with IE9.

To explain this visually I have prepared live demo. The demo has a 3D view of the scrollable “wrapper” element with “content” element inside of it which is shown underneath. To indicate when the “wrapper” receives “scroll” events I have added an event listener for the “scroll” event that flashes “scrolled” label for half a second. For the demo purpose, the wrapper’s overflow: hidden; was changed to overflow: auto; to enable scrolling using GUI. In addition, two buttons have been added besides bottom left corner of the “content” element to increase and decrease its height by ±40 pixels. This demo shows that increasing or decreasing content height when it is scrolled to top just changes its height but doesn’t change the scrollTop property neither fires the “scroll” event. But decreasing content height after scrolling it to the bottom will change the scrollTop property and fire the scroll event.

Yet, this is only the half of the way. If you have played with the demo you have probably noticed that the “scroll” event is fired only when content height is decreased. When its height is increased the content expands downward without changing the scrollTop property. In other words, the “scroll” event isn’t fired when content height is increased.

Part IV: Triggering the scroll event by increasing wrapper size

To make the “scroll” event fire when content height is increased we need to apply reverse logic. Instead of changing height of the “content” element we need to change height of the “wrapper” element itself while the height of the content element remains the same. This way, when the height of the “wrapper” element is increased its content will be pulled down forcing its scrollTop property to be changed and the “scroll” event to be fired.

This logic can be implemented by adding an absolutely positioned “innerWrapper” element inside the “content” element and telling it to “track” the content size by specifying 100% for its width and height properties. Then, by adding another content element inside the “innerWrapper” element having higher constant height will lead to the desired result: when the height of the outer “content” element will be increased the height of “innerWrapper” will be increased as well, but the height of the inner content will remain.

<div id="content" style="position:relative; width:100px; height:150px;">
    <div id="innerWrapper" style="position:absolute; width:100%; height:100%; overflow:hidden;">
        <div style="height: 200px;"></div>
    </div>
</div>
<script type="text/javascript">
    var innerWrapper = document.getElementById("innerWrapper"),
        content = document.getElementById("content");

    innerWrapper.scrollTop = innerWrapper.scrollHeight - innerWrapper.clientHeight;
    console.log("initial scrollTop: " + innerWrapper.scrollTop);

    innerWrapper.addEventListener("scroll", function() {
        console.log("innerWrapper scrolled, scrollTop: " + this.scrollTop);
    }, false);

    content.style.height = "160px";
    console.log("scrollTop after increasing content height: " + innerWrapper.scrollTop);
</script>

console output:

initial scrollTop: 50
scrollTop after increasing content height: 40
innerWrapper scrolled, scrollTop: 40

demo:

Part V: The final result

By merging both methods we can now create the final HTML structure and JavaScript code to watch for size changes in both directions and dimensions by listening to “scroll” events. Unlike previous examples the following example calculates and applies all the initial dimensions dynamically based on the dimension of an element with text and a default “serif” font-family. In addition the difference of ±50 pixels used in previous examples was replaced by ±1 pixel as this difference is enough to catch any size change larger than 1 pixel. And finally, instead of changing content size manually the code adds new font-family to the watched element.

console output on Chrome:

original content size: 213x18
setting new font family
innerWrapper scrolled, content size increased: 248x22

Update: Recently Adobe introduced the Adobe Blank font. Using this font as a fallback font removes the need to watch for decrease of element size because the size of an element with text having this font is 0.

Notes:

  • The “scroll” event handler may be called twice, once for each wrapper, if one size dimension is decreased and another is increased. Therefore new element size should be compared with its initial size to filter out secondary “scroll” event.
  • On some browsers, when adding event listeners right after setting scrollTop or scrollLeft properties will trigger the “scroll” event immediately. As with the previous case comparing new and initial element sizes will solve this issue.
  • As usual, this method doesn’t work with IE, at least not with IE9. Therefore for IE I recommend using regular method for watching element size – sampling element size with intervals.

As a convenient way to detect loaded fonts I have created “FontLoader” JavaScript class which is available for download on Github:

  Download on Github

Related resources:


comments powered by Disqus