Let's Make a Framework: Asynchronous Resource Loading

22 Sep 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.

The use of asynchronous script loaders has become very popular over the last few years. Rather than bundling assets into monolithic files to improve performance, it’s possible to coax browsers to load several files at once. There are different approaches to this, but in general JavaScript APIs are used rather than script elements.

I want to look at how asynchronous script loaders are built, what HTML5 provides, and then build a new asynchronous script loader as an advanced client-side tutorial. This tutorial series will be split over several weeks.

Popular Libraries

There are a huge amount of asynchronous script loaders out there. The diversity is largely due to the complexity of the problem space — different situations can be optimised, and browser support requirements play a huge role in the internal design of these libraries.

The first library I found and used actively was LABjs (GitHub: getify / LABjs, License: MIT) by Kyle Simpson. To understand what LABjs does, consider the following HTML:

<script src="jquery.js"></script>
<script src="jquery-ui.js"></script>
<script src="my-script.js"></script>

In the HTML4 spec, it says:

All SCRIPT elements are evaluated in order as the document is loaded

That means if a framework like jQuery is specified first, then subsequent scripts can safely depend on it being loaded. The downside of this, however, is downloading each of these scripts will block the loading of other page components like images and stylesheets. This can create a noticeable visual delay when the page is loaded.

Using LABjs can solve this problem:

<script type="text/javascript">
   $LAB
   .script("jquery.js").wait()
   .script("jquery-ui.js").wait()
   .script("my-script.js");
</script>

Here, every script will be loaded in parallel. The wait() method will cause a execution to delay until the script has finished loading and has been parsed.

Technically, code within the application’s client-side JavaScript could call $LAB.script() whenever a dependency is needed. This means a single page app could have a lower footprint until certain functionality is required.

Another popular library is RequireJS (GitHub: jrburke / requirejs, License: new BSD and MIT) by James Burke. RequireJS is very different to LABjs, with a large feature set:

  • Plugins
  • Server-side bundling and optimisation
  • Module support
  • Remote service dependencies (JSONP)
  • Script order control (through a plugin)
  • Web Worker support

The basic usage looks a bit like LABjs:

<script src="scripts/require.js"></script>
<script>
require(['script1.js', 'script2.js'], function(someModule) {
  // This optional callback runs when all dependencies are loaded
});
</script>

By embracing concepts like CommonJS Modules and the Asynchronous Module Definition API, RequireJS offers a way to not only avoid blocking when loading scripts, but also add structure to projects.

Yet another approach used by Modernizr and yepnope.js is to use a test to see if a script should be loaded:

yepnope({
  test: window.JSON,
  nope: 'json2.js',
  complete: function () {
    var data = window.JSON.parse('{ "json" : "string" }');
  }
});

The power here, is that you were able to parse a JSON string, but if your browser already had built in support for JSON parsing, it didn’t require you to load anything extra!

HTML5

JavaScript-powered asynchronous loading isn’t the end of the story. In HTML5, the script element has been updated to support async and defer attributes. This is already available in recent WebKit-based browsers and Firefox 3.6.

These attributes effectively let us do what LABjs offers:

  • If async is present, the script will be executed asynchronously, as soon as it is available
  • If defer is present, the script is executed when the page has finished parsing
  • If neither are present, the script is fetched and executed immediately before the page has finished parsing

From Mozilla’s documentation on the script element:

In older browsers that don’t support the async attribute, parser-inserted scripts block the parser; script-inserted scripts execute asynchronously in IE and WebKit, but synchronously in Opera and pre-4.0 Firefox.

Tony Gentilcore compared this approach to JavaScript libraries on the Surfin’ Safari Blog:

There are many clever techniques for working around this performance bottleneck, but they all involve extra code and browser-specific hacks. Instead, scripts which do not require synchronous execution can now be marked as either async or defer.

AMD

As well as HTML5, asynchronous script loading is addressed by the Asynchronous Module Definition API. To understand why this is useful, consider CommonJS Modules:

var fn = require('my-module').fn;
fn();

How would this work in a browser? Assuming 'my-module' is loaded through script tags, it would be difficult to control execution and parsing. With AMD it’s possible to do this instead:

define('my-module', ['require', 'exports', 'mydep'], function (require, exports, mydep) {
  exports.fn = function() {
    return 'hello from my-module:fn';
  }
});

References


blog comments powered by Disqus