Hey! I'm making a game called Terra Mango. You should check it out.

Non-onload-blocking Async JS with require.js

Stoyan Stefanov at phpied.com explained how to do non-onload-blocking async JS. TL;DR: window.onload can now fire before your async script loads and we don't trigger the browser's loading indicators (except in Opera).

I loved the method; but thought we could take it a bit further. I also wanted to use it with require.js.

And here is what I came up with:

// non-onload blocking async script adder that won't show the loading spinner.
(function(d,s){
    // Places an iframe in the document's head.
    // IE7 doesn't have document.head, and document.body.previousSibling might not be the head in modern browsers
    // so we get it the old fashioned way 
    with(d.getElementsByTagName("head")[0].appendChild(d.createElement('iframe')).contentWindow.document) {
        open().c=function(){
            // `this` here is the iframe's document (kinda cool!)
            s=this.createElement('script');
            s.setAttribute('data-main','/app/main');
            s.src='/app/require.js'; this.body.appendChild(s);
            };
        write('<body onload=document.c();>');
        close();
    }
}(document));

Yeah, yeah, I use with, I guess I was feeling a little evil. If you aren't as daring as I (heh), swap with out for a variable instead (it'll probably be about 0.01ms faster too!).

So, what's changed?

It's smaller.

This code isn't totally "standards compliant" - we're putting an iframe in the head. Doing this allows us to skip the part where we need to hide the iframe by setting its width, height, and border properties. #win

We don't have an ugly multi-line string and we don't have to worry about double-escaping any quotes in that string either. Stoyan's code had its JavaScript embedded in an html attribute; that could get confusing if you want to get fancy.
Instead, we add the function as a property of the iframe's document. Opera requires that we add property after the call to open(), which, lucky for us, returns the iframe's document object so we can make use of chaining.

<body onload> takes a millisecond or two longer to fire than a regular script tag. We fixed that. Note that we still need the body tag because of good-ol IE < 8 (if you don't need support for IE < 9 you can just use document.head everywhere). Ihárosi Wiktor pointed out that using just a script tag, as I had originally used, does block onload of the parent element. So we are back to using body onload.

Getting require.js to work.


Unfortunately, you can't just tell require.js what document to use without editing the file directly. The good news is that this is not that hard:

(function(document, navigator){
    /* RequireJS 2.0.2 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. Available via the MIT or new BSD license. see: http://github.com/jrburke/requirejs for details*/
    var requirejs,require,define;
    /* require.js goes here. Omitted for brevity. */
    function scripts() {
        return self.document.getElementsByTagName('script');
    }
    /* the rest require.js goes here. */
    this.requirejs = this.require = require;
    this.define = define;
}.call( parent.window, parent.document, parent.navigator ));

What we do here is just like Stoyan tells us to do, plus some.

First we wrap require.js in a modified IIFE - changing the way it's invoked so the context (this) of the function is the parent's window object. We then pass some of our parent window's global document and navigator objects as arguments so that require.js uses those instead of the iframe's.

Then we change one function within require.js in order for it to reference scripts within the iframe document, not our parents:

document.getElementsByTagName('script');
self.document.getElementsByTagName('script');

Now when we load require.js it should load all additional scripts within the parent document. If you are optimizing and concatenating with r.js you will need to do some additional work on the generated file. I'll leave that to you.

Comments for this blog entry