Asynchronous Resource Loading Part 3

13 Oct 2011 | By Alex Young | Tags frameworks tutorials lmaf network

Let’s Make a Framework is an ongoing series about building a JavaScript framework from the ground up.

These articles are tagged with lmaf. The project we’re creating is called Turing. Documentation is available at turingjs.com.

Previous parts:

HTML5 Asynchronous Loading Support

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

$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 Scripts

That means there are the following possible states:

  • async: Execute the script asynchronously as soon as it is available
  • defer: Execute the script when the page has finished parsing, thereby allowing other scripts to download and execute
  • async and defer: Use async if available, else legacy browsers will fall back to defer (from: Using HTML 5 for performance improvements)
  • If neither are present, the script is fetched and executed immediately before the page has finished parsing

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 Without Blocking 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 Order, 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 = false when execution order is important, else scriptTag.async = true could be used to load it and run it whenever possible.

Conclusion

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 c007251.

References


blog comments powered by Disqus