Friday, March 20, 2009

Downloading JavaScript Files in Parallel

Update: There were some bugs in the code here, which have been fixed. If you were using the older version of this script on your site, you should update it.
Also, a lot of people have got back to me about them getting undefined symbols, so I've highlighted parts of this post that are absolutely critical for it's functioning.

Steve Souders, creator of YSlow, author of the book High Performance Web Sites, is back in action writing a book titled Even Faster Websites. In it he details what one can do after the 14 YSlow rules (laws?) have been implemented, and you still want better better performance.

6 days ago, Steve gave a talk at SXSW Interactive, the slides of which are available on Slideshare. In it, he goes on to suggest (slide 27), that we should load scripts without blocking. To me, this seems like a very interesting idea, and based on his hints in the next few slides, I started experimenting.

Let me back up a little. When browsers download scripts, they block all other resources from downloading until such time that the script has finished downloading. When I mean all other resources, it includes even other CSS files and images. There are several ways to reduce the impact of serial script download, and the High Performance Websites book makes a couple of good suggestions.

  • Rule 1 - Make Fewer HTTP Requests
  • Rule 5 - Put Stylesheets at the Top
  • Rule 6 - Put Scripts at the Bottom
  • Rule 8 - Make JavaScript and CSS External
  • Rule 10 - Minify JavaScript

There are several others, mostly around caching strategies and reducing HTTP overhead, but these alone reduce the impact of serial script download significantly.

Now, if you haven't already taken care of these rules of optimization, my suggestion will be hard to implement, or will reap no benefits. You can stop reading now, and go back to your site to optimize your code.

Regular <script> tags:

Serial script download

See that waterfall pattern of file downloads? Nothing else is downloaded when a script is being downloaded, not even other scripts. The time gap between successive downloads is the time taken to evaluate the script, roughly proportional to the size of the script. Notice also that this takes in excess of 600ms even when the files are being served from my local machine, effectively having near-zero network latency.

With script parallelization

Parallel script download

Now, this looks much better, doesn't it? Not only has everything downloaded in parallel, it has taken less than half the time for the same size of files. In fact, you will notice that some noise when recording this data made the files take longer to download in this case, yet the total time was less than half of the pervious case.

So, how did I do this? Read on...

One script tag to rule them all

Reading into Souders' suggested techniques (If you still have that slideshow open, go to slide 26), I decided that what I wanted was:

  • I want the browser busy indicators to show,
  • I want a script that doesn't care if I'm downloading from the same domain or not, and
  • I wanted to preserve the evaluation order in case of interdependent scripts.

So, I came up with the following little script:

<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", doc = document, ua = window.navigator.userAgent;
for(var i=0, l=s.length; i<l; ++i) {
if(ua.indexOf("Firefox")!==-1 || ua.indexOf("Opera")!==-1) {
var t=doc.createElement(sc);
t[sa]("src", s[i]);
t[sa]("type", tp);
doc.getElementsByTagName("head")[0].appendChild(t);
} else {
doc.writeln("<" + sc + " type=\"" + tp + "\" src=\"" + s[i] + "\"></" + sc + ">");
}
}
})();
</script>

It's a little ugly: There's a user-agent sniff happening in there and I've written it so as to use the least number of bytes possible (save for the indentation), but it works, and has no dependencies on any library. Just modify the array on top to place your list of scripts you wish to include, and let the rest of the script do the dirty work. If you need to include scripts from another domain, just use an absolute path.

This code was written keeping in mind that this should be the only piece of on-page inline JavaScript. This can then go on and fetch the script required in parallel, and those in turn can unobtrusively do their thing.

This works as advertised in IE and FF. I haven't tested the parallelization in other browsers, but at least it fails gracefully and preserves script execution order. This has now been tested in Safari, Chrome and Opera as well, and works as advertised everywhere.


Find this interesting? Follow me on twitter at @rakesh314 for more.

ShareThis