Coupling asynchronous scripts

December 27, 2008 11:32 pm | 14 Comments

This post is based on a chapter from Even Faster Web Sites, the follow-up to High Performance Web Sites. Posts in this series include: chapters and contributing authors, Splitting the Initial Payload, Loading Scripts Without Blocking, Coupling Asynchronous Scripts, Positioning Inline Scripts, Sharding Dominant Domains, Flushing the Document Early, Using Iframes Sparingly, and Simplifying CSS Selectors.

 

Much of my recent work has been around loading external scripts asynchronously. When scripts are loaded the normal way (<script src="...">) they block all other downloads in the page and any elements below the script are blocked from rendering. This can be seen in the Put Scripts at the Bottom example. Loading scripts asynchronously avoids this blocking behavior resulting in a faster loading page.

One issue with async script loading is dealing with inline scripts that use symbols defined in the external script. If the external script is loading asynchronously without thought to the inlined code, race conditions may result in undefined symbol errors. It’s necessary to ensure that the async external script and the inline script are coupled in such a way that the inlined code isn’t executed until after the async script finishes loading.

There are a few ways to couple async scripts with inline scripts.

  • window’s onload – The inlined code can be tied to the window’s onload event. This is pretty simple to implement, but the inlined code won’t execute as early as it could.
  • script’s onreadystatechange – The inlined code can be tied to the script’s onreadystatechange and onload events.  (You need to implement both to cover all popular browsers.) This code is lengthier and more complex, but ensures that the inlined code is called as soon as the script finishes loading.
  • hardcoded callback – The external script can be modified to explicitly kickoff the inlined script through a callback function. This is fine if the external script and inline script are being developed by the same team, but doesn’t provide the flexibility needed to couple 3rd party scripts with inlined code.

In this blog post I talk about two parallel (no pun intended) issues: how async scripts make the page load faster, and how async scripts and inline scripts can be coupled using a variation of John Resig’s Degrading Script Tags pattern. I illustrate these through a recent project I completed to make the UA Profiler results sortable. I did this using Stuart Langridge’s sorttable script. It took me ~5 minutes to add his script to my page and make the results table sortable. With a little more work I was able to speed up the page by more than 30% by using the techniques of async script loading and coupling async scripts.

Normal Script Tags

I initially added Stuart Langridge’s sorttable script to UA Profiler in the typical way (<script src="...">), as seen in the Normal Script Tag implementation. The HTTP waterfall chart is shown in Figure 1.

Normal Script Tags
Figure 1: Normal Script Tags HTTP waterfall chart

The table sorting worked, but I wasn’t happy with how it made the page slower. In Figure 1 we see how my version of the script (called “sorttable-async.js”) blocks the only other HTTP request in the page (“arrow-right-20×9.gif”), which makes the page load more slowly. These waterfall charts were captured using Firebug 1.3 beta. This new version of Firebug draws a vertical red line where the onload event occurs. (The vertical blue line is the domcontentloaded event.) For this Normal Script Tag version, onload fires at 487 ms.

Asynchronous Script Loading

The “sorttable-async.js” script isn’t necessary for the initial rendering of the page – sorting columns is only possible after the table has been rendered. This situation (external scripts that aren’t used for initial rendering) is a prime candidate for asynchronous script loading. The Asynchronous Script Loading implementation loads the script asynchronously using the Script DOM Element approach:

var script = document.createElement('script');
script.src = "sorttable-async.js";
script.text = "sorttable.init()"; // this is explained in the next section
document.getElementsByTagName('head')[0].appendChild(script);

The HTTP waterfall chart for this Asynchronous Script Loading implementation is shown in Figure 2. Notice how using an asynchronous loading technique avoids the blocking behavior – “sorttable-async.js” and “arrow-right-20×9.gif” are loaded in parallel. This pulls in the onload time to 429 ms.

Async Script Loading
Figure 2: Asynchronous Script Loading HTTP waterfall chart

John Resig’s Degrading Script Tags Pattern

The Asynchronous Script Loading implementation makes the page load faster, but there is still one area for improvement. The default sorttable implementation bootstraps itself by attaching “sorttable.init()” to the onload handler. A performance improvement would be to call “sorttable.init()” in an inline script to bootstrap the code as soon as the external script was done loading. In this case, the “API” I’m using is just one function, but I wanted to try a pattern that would be flexible enough to support a more complex situation where the module couldn’t assume what API was going to be used.

I previously listed various ways that an inline script can be coupled with an asynchronously loaded external script: window’s onload, script’s onreadystatechange, and hardcoded callback. Instead, I used a technique derived from John Resig’s Degrading Script Tags pattern. John describes how to couple an inline script with an external script, like this:

<script src="jquery.js">
jQuery("p").addClass("pretty");
</script>

The way his implementation works is that the inlined code is only executed after the external script is done loading. There are several benefits to coupling inline and external scripts this way:

  • simpler – one script tag instead of two
  • clearer – the inlined code’s dependency on the external script is more obvious
  • safer – if the external script fails to load, the inlined code is not executed, avoiding undefined symbol errors

It’s also a great pattern to use when the external script is loaded asynchronously. To use this technique, I had to change both the inlined code and the external script. For the inlined code, I added the third line shown above that sets the script.text property. To complete the coupling, I added this code to the end of “sorttable-async.js”:

var scripts = document.getElementsByTagName("script");
var cntr = scripts.length;
while ( cntr ) {
    var curScript = scripts[cntr-1];
    if ( -1 != curScript.src.indexOf('sorttable-async.js') ) {
        eval( curScript.innerHTML );
        break;
    }
    cntr--;
}

This code iterates over all scripts in the page until it finds the script block that loaded itself (in this case, the script with src containing “sorttable-async.js”). It then evals the code that was added to the script (in this case, “sorttable.init()”) and thus bootstraps itself. (A side note: although the line of code was added using the script’s text property, here it’s referenced using the innerHTML property. This is necessary to make it work across browsers.) With this optimization, the external script loads without blocking other resources, and the inlined code is executed as soon as possible.

Lazy Loading

The load time of the page can be improved even more by lazyloading this script (loading it dynamically as part of the onload handler). The code behind this Lazyload version just wraps the previous code within the onload handler:

window.onload = function() {
    var script = document.createElement('script');
    script.src = "sorttable-async.js";
    script.text = "sorttable.init()";
    document.getElementsByTagName('head')[0].appendChild(script);
}

This situation absolutely requires this script coupling technique. The previous bootstrapping code that called “sorttable.init()” in the onload handler won’t be called here because the onload event has already passed. The benefit of lazyloading the code is that the onload time occurs even sooner, as shown in Figure 3. The onload event, indicated by the vertical red line, occurs at ~320 ms.

Lazyloading
Figure 3: Lazyloading HTTP waterfall chart

Conclusion

Loading scripts asynchronously and lazyloading scripts improve page load times by avoiding the blocking behavior that scripts typically cause. This is shown in the different versions of adding sorttable to UA Profiler:

The times above indicate when the onload event occurred. For other web apps, improving when the asynchronously loaded functionality is attached might be a higher priority. In that case, the Asynchronous Script Loading version is slightly better (~400 ms versus 417 ms). In both cases, being able to couple inline scripts with the external script is a necessity. The technique shown here is a way to do that while also improving page load times.

14 Responses to Coupling asynchronous scripts

  1. Steve, is it OK, that timingz for all identical files on all diagrams are different? I think these models require more precise measurement.

    Also I think that scripts sometimes are more important than images or other resources — they delive client side logic. So we need to launch their load on ‘combined’ window.onload (defer / onDOMready / etc) event, not on standard one.

  2. Interesting that the Lazyloading version does not work in Opera with “Delayed Script Execution” enabled – https://stevesouders.com/blog/2008/09/11/delayed-script-execution-in-opera/ – at least not in my Opera 10 alpha on OSX.

  3. @sunnybear: The timings will be different since it involves live HTTP requests going over the Internet. I ran a longer test (100 measurements) using Hammerhead ( https://stevesouders.com/hammerhead/ ) and saw a 22% savings. Lazyloading scripts is best suited for code that is not critical to rendering the initial page, as in this example.

    @Michael: Yes, that setting in Opera is an edge case that would require further testing.

  4. Hi Steve,

    Doesn’t adding a call to “sorttable.init();” at the end of the “sorttable-async.js” file have the same effect. The file gets downloaded async with the script tag injection but without the script.text and after all the contents get evaluated, the last line “sorttable.init();” triggers a call to the init just like above but without using the script.text.

    Thank You,
    Vish

  5. @Vish: I was looking for a more flexible pattern for integrating 3rd party scripts. Although in this case the 3rd party script only has one API function, that’s typically not the case. For typical libraries (jQuery, Google Analytics) the 3rd party developer can’t hardwire the API functions at the bottom of the module.

  6. @Vish, @Steve: we can use another pattern for such scripts: check time to time if main funciton is loaded (i.e. urchinTracker).

    I think there should be an article about different approaches to load scripts unobtrusively written. I mean — sometimes we can use simple John’s approach, sometimes — your LazyLoading. Sometimes — onReadyExecution (that I’ve already described in my book in Russian, for GA):
    var _on_ready_execution = setInterval(function() {
    if (typeof urchinTracker === ‘function’) {
    urchinTracker();
    clearInterval(_on_ready_execution);
    }
    }, 10);

    Also we can include all function calls into main script, as Vish has noticed.

  7. @sunnybear: I’m planning on including this topic in my next book. I’ll certainly add your polling technique, although I prefer using callbacks to polling.

  8. Nice post. Very informative.

    I’ve read up on placing JS includes right before the . Heard about that through YSlow. But the other content you provide is really interesting. Thanks!

  9. Thanks, Steve.

    We have been using dynamic script DOM element and it does improve the site speed a lot.

    just want to share one small lesson we learnt from our practice on dynamic script DOM element: it seems that the dynamic script DOM element is not fully compatible with firebug. It is more due to a firebug’s bug (http://code.google.com/p/fbug/issues/detail?id=721) but the effect is very inconvenient for web developers.

    Basically, with dynamic script DOM element, firebug’s console might be disabled (if any asynchronously loaded script access variable “console”, even check typeof “console”).

    One way we resolved this is to have “window.loadFirebugConsole && window.loadFirebugConsole();” in a static script tag before any dynamic script DOM element is loaded. This single line of work-around spent us a few days so I guess it might be worth to share here.

  10. Steve. According to my previous notice I’d like to share an example of async scripts loading –
    http://webo.in/tests/yass-tree-load/
    There 2 main cases
    1. async scripts are loading in some kind of dependency tree (with recursive post-load handlers). Onload / onreadystatechange is used to run handlers and linked innerHTML code (is required only for Opera – as ‘ve noticed – all the other browsers handles dual scripts well).
    2. Secondly there is a JS-counter example. Its onload handler defined via title attribute and this approach can be used for fully external scripts — i.e. GA from my previous reply.

    Of course thanks for Onload / Onreadystatechange approach — it really works.

    Maybe this will help somehow somebody…

  11. When you can’t post-load (due to several reasons), you can always pretend:

    http://blog.choonkeat.com/weblog/2009/03/keeping-google-ads-documentwrite-and-browsers-all-happy.html

  12. Hi Steve,

    Could you please look at my question about the race condition in js and asynchronize loading as seen in the link below

    http://www.codingforums.com/showthread.php?p=816318#post816318

  13. Coupling asynchronous scripts is just what I was looking for but I am having difficulty with asynch loading also.

  14. @sunnybear interesting idea with polling.