Making the browser download scripts in parallel.

Current browsers download scripts serially.  What that means is that if you use:

<script type="text/javascript" src="foobar.js"></script>
<script type="text/javascript" src="quuxdoo.js"></script>

The browser will not fetch quuxdoo.js until it has finished downloading foobar.js.   In fact, it will not download any other resource until that script is downloaded.  This is really a sad state of affairs.

An existing “solution”

An article “Downloading JavaScript files in Parallel” by Rakesh Pai explains the issue with a couple screenshots displaying the Firebug Net panel.

<script type="text/javascript">
    (function() {
        var s = [
            "/javascripts/script1.js",
            "/javascripts/script2.js",
            "/javascripts/script3.js",
            "/javascripts/script4.js",
            "/javascripts/script5.js"
        ];

        var sc = "script", tp = "text/javascript", sa = "setAttribute";
        for(var i=0, l=s.length; i<l; ++i) {
            if(window.navigator.userAgent.indexOf("MSIE")!==-1 || window.navigator.userAgent.indexOf("WebKit")!==-1) {
                document.writeln("<" + sc + " type=\"" + tp + "\" src=\"" + s[i] + "\" defer></" + sc + ">");
            } else {
                var t=document.createElement(sc);
                t[sa]("src", s[i]);
                t[sa]("type", tp);
                document.getElementsByTagName("head")[0].appendChild(t);
            }
        }
    })();
</script>

He provides a solution that seems to work well, but contains a few things I didn’t like or consider bugs:

  1. Browser sniffing.  Do Firefox and Opera not support the document.write() method? No, really, I’m asking here.  I don’t have a copy of FF 1.5 or 2.0 lying around to verify.  The latest version of Opera does seem to indeed.  And not just that, the script checks for the browser per iteration of the loop, so if you have 10 scripts that you load with this method, the browser check will occur 10 times!
  2. The code tries to optimize for space but ends up being harder to read instead.  This should ideally be the job of a minifier like YUI Compressor, Douglas Crockford’s JSMIN, Dojo Toolkit’s ShrinkSafe, or Dean Edwards’ Packer.  A couple of those space-saving tricks can be used to hint the minifier better while keeping the code readable.
  3. Take HTML 5.  New script element attributes like async and defer aren’t handled.  Both are defined as boolean attributes and async=”true” means the script would execute asynchronously as soon as it is available. What about scripts that aren’t of mime type text/javascript?  A better approach would have been to consume an array of attribute objects and fill those attributes into the created script element.
  4. Loop invariants should be outside the loop especially in a language like JavaScript where linear scope chains determine how quickly a variable can be accessed.  The code negligently accesses global objects and repeatedly goes down nested objects, which it really doesn’t need to per iteration.
  5. XHTML isn’t dead just yet.  Attribute minimization (ref. “<script … defer></script>”) is not valid XHTML.  Language purists may cringe.

Where’s the code?

Here is a non-browser sniffing version that offers the above flexibility.  Please note, as of this time I do not have access to any version of the-browser-that-shall-not-be-named (blah, pottermania), so I’m relying on the community to test this piece of code in the-browser-that-shall-not-be-named.  Enough said.  Here’s my attempt at the code:

/**
 * Loading scripts in parallel.
 * Copyright (C) 2009 Yesudeep Mangalapilly.
 *
 * Licensed under the terms of the MIT License.
 */
function getScripts(scripts){
    var lt = "%3C",
        gt = "%3E",
        doc = document,
        len = scripts.length,
        html = [];

    for (var i = 0, script = null, attribute_string = null; i < len; ++i){
        script = scripts[i];
        switch (typeof script){
        case 'string':
            attribute_string = ['src=\"', decodeURI(script), '\"'].join('');
            break;
        case 'object':
            var attrs = [];
            for (var key in script){
                var value = script[key];
                switch(key){
                    case 'src':
                    case 'SRC':
                        value = decodeURI(value);
                        break;
                    default: break;
                }
                attrs.push([key, '\"' + value + '\"'].join('='));
            }
            attribute_string = attrs.join(' ');
            break;
        default:
            continue;
        }
        html.push(lt, 'script ', attribute_string, gt, lt, '/script', gt);
    }
    html = unescape(html.join(''));
    doc.write(html);
}

Usage

Here is how you can use the code:

var scripts = [
    {
        src: "foobar.js",
        async: true
    },
    "quuxdoo.js"
];

getScripts(scripts);

Hope that helps.  Suggestions and constructive criticism most welcome.

About these ads

About this entry