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.

Advertisements

8 thoughts on “Making the browser download scripts in parallel.”

  1. Hi Yesudeep,

    Thanks for this post. While you have a couple of good ideas here (which I shall incorporate in my script as well), I think your design goals are different from mine. This is not to say that your or my script are wrong – it’s just different approaches with different results.

    In my case – and I agree I haven’t articulated this well enough on my site – the design goal was that the script block is the only script block on the page, and not an external HTTP request, for an extra level of optimization. So I needed it to be concise. A second design goal was that the script execution order should be maintained – hence I didn’t care about the HTML5 async attribute. I think it not only makes the script more concise, it also fits a developers mental model of the execution of the script.

    The browser sniffing was required because of the difference in behavior of document.write and script tag injection as far as parallel downloads are concerned, in different browsers. I haven’t tested your script, but on the surface it looks like you won’t achieve parallel downloads in IE. This also maps to the Steve Souders presentation I’ve linked to in my post.

    While I agree that the manual minification I’ve done makes code less readable, my design goal wasn’t readability so much as conciseness. The manual minification wasn’t done to bypass minification scripts, but to minify better with it. Because of the way most of the minification scripts load symbol graphs, my code minifies better – and that’s why I chose that route.

    Point taken about the loop invariants inside the loop. Wonder how I missed it, even though I’ve written about this before. My bad. I shall incorporate it in my script.

    I don’t particularly care about XHTML, but you could feel free to disagree. About language purists – and while that’s a separate debate – my script generates pure spec compliant HTML 4.01 Transitional code, and complies with HTML 5 specs as well. This will also be compliant markup in all sites that claim they are XHTML but handle doctypes and mimetypes incorrectly, which is a surprisingly huge majority, in which case the rendering mode falls back to quirks anyway.

    Cheers!

  2. @rakesh

    Hi Rakesh,

    [Please note this was written as a response on your blog, I’m simply reproducing it here for continuity. And this was before I saw your comment here.]

    Ah, I do see our design goals are different. I’m pretty sure you have your reasons to disagree, and that’s appreciated.

    Certainly, you wouldn’t want to include a ton of scripts into your code. Minimizing the number of HTTP requests made by combining scripts is surely a production tip. However, when you are testing code on a development server, it really doesn’t make sense to have all of them combined as debugging them then becomes painful (ref. error foobar on line 9456 in entire.library.js or line 1 in entire.library.min.js).

    Anyway, including a lot of scripts wasn’t my point in the blog post if you noticed.

    Factoring out loop invariants and caching access to global variables does speed up code and to my mind, there’s no reason to not do this. As for the number of bytes consumed over-the-wire, do you really think t[sa] makes a difference for example? If you’re using gzip compression to serve your script files, and I’m sure you would, t.setAttribute, t.setAttribute, t.setAttribute, … becomes a non-issue. There don’t appear to be any benefits doing that, for example.

    For arguments sake, if you’re using a UNIX or Linux, you can try sticking this list of words into a file:

    Apple
    Google
    Microsoft
    Orange
    Mango
    Tomato
    Mongo
    China
    Charlie
    Delta
    Gamma
    Beta

    and run `gzip filename.txt` on that.

    Now try that on this input:

    Apple
    Apple
    Apple
    Apple
    Apple
    Apple
    Apple
    Apple
    Apple
    Apple
    Apple
    Apple
    Apple
    Apple
    Apple
    Apple
    Apple
    Apple
    Apple
    Apple
    Apple
    Apple

    I’m pretty sure you’ll see a large difference.

    var sa = ‘setAttribute’;
    t[sa](foo, bar); is surely harder to read than t.setAttribute(foo, bar);. Don’t you agree? 🙂

    Additionally, look at this line:

    doc.getElementsByTagName(“head”)[0].appendChild(t);

    This is inside a loop. doc.getElementsByTagName(“head”)[0], again is a loop invariant (it fetches the same DOM element per iteration and DOM is pretty damn slow!), and can be pretty easily factored out of it.

    Also, which version of Firefox or Opera do you see doesn’t download scripts in parallel when we use document.write()? As I said, I do not have access to to Firefox 2.0/Opera 9.x or earlier and IE. With the latest release of both browsers, document.write() does cause the browser to fetch scripts in parallel.

    I’d appreciate any help with this and would be more than happy to document them and incorporate them into my version as well. Thanks for taking the time for answering. 🙂

    Cheers,
    Yesudeep.

  3. Hey. Sorry, forgot to notify you that I’ve changed the code on my blog now to make it work across even more browsers. Safari and Opera were being ignored, and it was giving undefined symbol errors in both if scripts had dependencies and script execution order had to be preserved.

    Cheers!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s