Asynchronous Resource Loading Part 3

2011-10-13 00:00:00 +0100 by Alex R. Young
*Let's Make a Framework* is an ongoing series about building a JavaScript framework from the ground up. These articles are tagged with [lmaf](http://dailyjs.com/tags.html#lmaf). The project we're creating is called [Turing](http://github.com/alexyoung/turing.js). Documentation is available at [turingjs.com](http://turingjs.com/).

Previous parts:

HTML5 Asynchronous Loading Support

In the last part I created this simple API for loading scripts

$t.require('/load-me.js', function() {
  // Loaded

The next step is to look at how to handle execution order control. The
script element gets two new attributes in HTML5:
async and defer. Technically
defer was present in HTML4:

When set, this boolean attribute provides a hint to the user agent that the script is not going to generate any document content (e.g., no "document.write" in javascript) and thus, the user agent can continue parsing and rendering.

From: HTML4

That means there are the following possible states:

Out of interest, I tried setting these attributes on the generated
script elements, and it didn't cause IE6 to break. I
suspect this should really use feature detection to be safe.

In order to support the baseline HTML5 API, I added some options to the
require method:

$t.require('/load-me.js', { async: true, defer: true }, function() {
  assert.equal(loadMeDone, 1);

And this just sets the properties as you'd expect:

function require(scriptSrc, options, fn) {
  var script = document.createElement('script');
  script.type = 'text/javascript';
  script.src = scriptSrc;

  if (options.async) {
    script.async = options.async;

  if (options.defer) {
    script.defer = options.defer;

Execution Order and Preloading

Now require works as asynchronously as possible, have we
solved the problem of asynchronous resource loading? Not by a long shot.
Although execution order could now be controlled through callbacks, this
wouldn't do what we really want to do:

// This is wrong:
$t.require('/library-a.js', function() {
  $t.require('/library-b.js', function() {
    $t.require('/app-code.js', function() {
      // Done!

Why is this wrong? It fails to distinguish between loading and
executing scripts. Libraries like LABjs utilise multiple strategies for managing script execution order. Going back to Loading Scripts

by Steve Souders, we can find six techniques for downloading scripts
without blocking. Steve even includes a table to compare the properties
of each of these approaches, and it shows that only three techniques can
be used to control execution order.

There are more techniques available -- many libraries use
object or img to preload scripts. A
script element is used once the script has loaded,
effectively rerequesting the script when it's required, and therefore
hitting the cache. This preloading approach is fairly widely used, but
has to account for a lot of browser quirks. In some cases it can cause
scripts to be loaded twice.

XMLHttpRequest Loading

LABjs has a whole load of code for managing loading scripts with
XMLHttpRequest. Why? Well, it makes preloading possible and avoids
loading scripts twice. However, it can only be used to load local
scripts due to the same origin policy.

Dynamic Script Execution Order

In Dynamic Script Execution
, an
extension to the behaviour of the async attribute is
considered. This proposal is known as async=false:

If a parser-inserted script element has the `async` attribute present, but its value is exactly "false" (or any capitalization thereof), the script element should behave EXACTLY as if no `async` attribute were present.

This would allow our script loader to set scriptTag.async =
when execution order is important, else
scriptTag.async = true could be used to load it and run it
whenever possible.


Despite script loading libraries like LABjs and
RequireJS existing for a few years, the problem still hasn't been completely solved, and we still need to support legacy
browsers. Simply loading non-blocking JavaScript by inserting script
tags is possible, but controlling execution order requires an inordinate
amount of effort.

If you've ever wondered why LABjs is around 500 lines of code, then I
hope you can now appreciate the lengths the author has gone to!

The HTML5 support I added can be found in commit