Non-blocking JavaScript Downloads

By YUI TeamJuly 22nd, 2008

Stoyan Stefanov.About the Author: Stoyan Stefanov is a Yahoo! web developer working for the Exceptional Performance team and leading the development of the YSlow performance tool. He also an open-source contributor, conference speaker and technical writer: his latest book is called Object-Oriented JavaScript.

External JavaScript files block downloads and hurt your page performance, but there is an easy way to work around this problem: use dynamic scripts tags and load scripts in parallel, improving the page loading speed and the user experience.

The problem: scripts block downloads

Let’s first take a look at what the problem is with the script downloads. The thing is that before fully downloading and parsing a script, the browser can’t tell what’s in it. It may contain document.write() calls which modify the DOM tree or it may even contain location.href and send the user to a whole new page. If that happens, any components downloaded from the previous page may never be needed. In order to avoid potentially useless downloads, browsers first download, parse and execute each script before moving on with the queue of other components waiting to be downloaded. As a result, any script on your page blocks the download process and that has a negative impact on your page loading speed.

Here’s how the timeline looks like when downloading a slow JavaScript file (exaggerated to take 1 second). The script download (the third row in the image) blocks the two-by-two parallel downloads of the images that follow after the script:

Timeline - Blocking behavior of JavaScript files

Here’s the example to test yourself.

Problem 2: number of downloads per hostname

Another thing to note in the timeline above is how the images following the script are downloaded two-by-two. This is because of the restriction of how many components can be downloaded in parallel. In IE <= 7 and Firefox 2, it’s two components at a time (following the HTTP 1.1 specs), but both IE8 and FF3 increase the default to 6.

You can work around this limitation by using multiple domains to host your components, because the restriction is two components per hostname. For more information of this topic check the article “Maximizing Parallel Downloads in the Carpool Lane” by Tenni Theurer.

The important thing to note is that JavaScripts block downloads across all hostnames. In fact, in the example timeline above, the script is hosted on a different domain than the images, but it still blocks them.

Scripts at the bottom to improve user experience

As Yahoo!’s Performance rules advise, you should put the scripts at the bottom of the page, towards the closing </body> tag. This doesn’t really make the page load faster (the script still has to load), but helps with the progressive rendering of the page. The user perception is that the page is faster when they can see a visual feedback that there is progress.

Non-blocking scripts

It turns out that there is an easy solution to the download blocking problem: include scripts dynamically via DOM methods. How do you do that? Simply create a new <script> element and append it to the <head>:

var js = document.createElement('script');
js.src = 'myscript.js';
var head = document.getElementsByTagName('head')[0];
head.appendChild(js);

Here’s the same test from above, modified to use the script node technique. Note that the third row in the image takes just as long to download, but the other resources on the page are loading simultaneously:

Non-blocking JavaScript timeline

Test example

As you can see the script file no longer blocks the downloads and the browser starts fetching the other components in parallel. And the overall response time is cut in half.

Dependencies

A problem with including scripts dynamically would be satisfying the dependencies. Imagine you’re downloading 3 scripts and three.js requires a function from one.js. How do you make sure this works?

Well, the simplest thing is to have only one file, this way not only avoiding the problem, but also improving performance by minimizing the number of HTTP requests (performance rule #1).

If you do need several files though, you can attach a listener to the script’s onload event (this will work in Firefox) and the onreadystatechange event (this will work in IE). Here’s a blog post that shows you how to do this. To be fully cross-browser compliant, you can do something else instead: just include a variable at the bottom of every script, as to signal “I’m ready”. This variable may very well be an array with elements for every script already included.

Using YUI Get utility

The YUI Get Utility makes it easy for you to use script includes. For example if you want to load 3 files, one.js, two.js and three.js, you can simply do:

var urls = ['one.js', 'two.js', 'three.js'];
YAHOO.util.Get.script(urls);

YUI Get also helps you with satisfying dependencies, by loading the scripts in order and also by letting you pass an onSuccess callback function which is executed when the last script is done loading. Similarly, you can pass an onFailure function to handle cases where scripts fail to load.

var myHandler = {
    onSuccess: function(){
        alert(':))');
    },
    onFailure: function(){
        alert(':((');
    }
};

var urls = ['1.js', '2.js', '3.js'];
YAHOO.util.Get.script(urls, myHandler);

Again, note that YUI Get will request the scripts in sequence, one after the other. This way you don’t download all the scripts in parallel, but still, the good part is that the scripts are not blocking the rest of the images and the other components on the page. Here’s a good example and tutorial on using YUI Get to load scripts.

YUI Get can also include stylesheets dynamically through the method
YAHOO.util.Get.css() [example].

Which brings us to the next question:

And what about stylesheets?

Stylesheets don’t block downloads in IE, but they do in Firefox. Applying the same technique of dynamic inserts solves the problem. You can create dynamic link tags like this:

var h = document.getElementsByTagName('head')[0];
var link = document.createElement('link');
link.href = 'mycss.css';
link.type = 'text/css';
link.rel = 'stylesheet';
h.appendChild(link);

This will improve the loading time in Firefox significantly, while not affecting the loading time in IE.

Another positive side effect of the dynamic stylesheets (in FF) is that it helps with the progressive rendering. Usually both browsers will wait and show blank screen until the very last piece of stylesheet information is downloaded, and only then they’ll start rendering. This behavior saves them the potential work of re-rendering when new stylesheet rules come down the wire. With dynamic <link>s this is not happening in Firefox, it will render without waiting for all the styles and then re-render once they arrive. IE will behave as usual and wait.

But before you go ahead and implement dynamic <link> tags, consider the violation of the rule of separation of concerns: your page formatting (CSS) will be dependent on behavior (JS). In addition, this problem is going to be addressed in future Firefox versions.

Other ways?

There are other ways to achieve the non-blocking scripts behavior, but they all have their drawbacks.

Method Drawback
Using defer attribute of the script tag IE-only, unreliable even there
Using document.write() to write a script tag
  1. Non-blocking behavior is in IE-only
  2. document.write is not a recommended coding practice
XMLHttpRequest to get the source then execute with eval().
  1. eval() is evil”
  2. same-domain policy restriction
XHR request to get the source, create a new script tag and set its content
  1. more complex
  2. same-domain policy
Load script in an iframe
  1. complex
  2. iframe overhead
  3. same-domain policy

Future

Safari and IE8 are already changing the way scripts are getting loaded. Their idea is to download the scripts in parallel, but execute them in the sequence they’re found on the page. It’s likely that one day this blocking problem will become negligible, because only a few users will be using IE7 or lower and FF3 or lower. Until then, a dynamic script tag is an easy way around the problem.

Summary

  • Scripts block downloads in FF and IE browsers and this makes your pages load slower.
  • An easy solution is to use dynamic <script> tags and prevent blocking.
  • YUI Get Utility makes it easier to do script and style includes and manage dependencies.
  • You can use dynamic <link> tags too, but consider the separation of concerns first.

35 Comments

  1. I’ve been using this method via the YUILoader from quite some time now.

    This also means that the combo-filter that has been talked about here recently might not be a better approach.

  2. Hi,

    I saw Steven Souders discussing much of this at Google I/O (http://sites.google.com/site/io/even-faster-web-sites). Maybe I’m wrong, but shouldn’t he receive some credit, or at least a mention in this post?

  3. It’s worth noting that you should *only* use the CSS loading technique for CSS that depends on JavaScript functionality. For example, several of the YUI widgets include layout CSS and style CSS – these would be ideal candidates to load in after the initial page load, since they only apply to JavaScript-created widgets.

    Loading some or all of the general site styling CSS with JavaScript will leave users with JavaScript disabled with a partially or completely unstyled site. If you have a performance issue loading your site CSS, there are probably better techniques you can use (minification and/or concatenation) to speed that up without breaking the separation of concerns between CSS and JavaScript.

  4. Hi Eric, why not use both? I mean, use the DOM include method to include a single script created with the combo handler?

  5. @Eric — Keep in mind that YUI Loader *does* serialize the JS downloads — so if it loads five files, it does so one at a time (although those five loads don’t block other downloads taking place at the same time). The best of all worlds is YUI Loader using the Combo Handler; we’ll try to have that for you in the next release. -Eric

  6. @Steve – very good point about the difference between JS-related/widget CSS vs. general site layout CSS!

  7. @Todd – Steve did great work for Yahoo’s Exceptional Performance team while he was here (he was my colleague on the Exceptional Performance team), and he’s obviously continued that at Google. The non-blocking script technique described in this article was not derived from Steve’s work, and it’s something I’ve been promoting internally at Yahoo for quite awhile. I gather Steve has made the same discovery as he continues his own research in this area, and so were msn.com, Yahoo! China and probably others.

  8. You can also use multiple separate Get calls to download the JavaScript files simultaneously if the download order isn’t important. Depending on the browser that will either download all the files at once or the 2 per domain. Though you are left on your own to determine when they’ve all been downloaded.

  9. Hi,

    I see that you don’t specify the type for the script element, like: js.setAttribute(“type”, “text/javascript”). Is this a mandatory condition for breaking the script blocking rule?
    I’ve been using this method of adding script elements into HEAD of some years now, but as far as I know, the js script was still blocking everything else. Do you think that setting the type of the script has anything to do with this?

  10. [...] rendering isn’t very good. I can also understand the well-written and interesting discussion of non-blocking Javascript downloads posted by Stoyan Stefanov at the YUI Blog, which clearly explains why and how Javascript can mess [...]

  11. When you say “put scripts on the bottom”, does that go for YUI only or all scripts?
    Thanks!

  12. YAHOO.util.Get.script loading order is not reliable in Safari < 3.0.

  13. @James: good point!

    @Daniel R: I left out the type attribute for brevity. With and without it should be the same thing – scripts added dynamically with DOM methods will not block

    @Jessica: scripts at the bottom is a Yahoo! performance rule valid for all JavaScripts. More info here: http://developer.yahoo.com/performance/rules.html#js_bottom
    The article above though describes an alternative approach, where you can keep the scripts in the head, but add them with DOM method, not directly as <script…> tags.

  14. [...] Stefanov writes about Non-Blocking JavaScript Downloads on the Yahoo User Interface Blog.  Good content all around, but I’d like to add a note on an [...]

  15. The Dojo JavaScript Toolkit allows you to load JS module files in this way (dynamically appended script tags) via the xdomain loader. Another benefit of doing an xdomain Dojo build: it works out the module dependencies correctly.

    In contrast to the YUILoader, Dojo’s xdomain loader loads the script tags as soon as it see the dojo.require call: it does not serially load one, then wait for it to load before loading the other one.

    Plus, the dojo.require functionality allows you to load modules after the page loads, so you can speed up your initial load even faster by just loading a bare minimum of modules first.

    YUI could look at using the Dojo loader to get these benefits. I did a prototype that loaded YUI via the dojo loader.

  16. Interesting method.
    I had know only ‘iframe’ method to loads scripts non-blocking.

    The problem with outer scripts is that they usually should be placed in specific place of document, for social bookmarks, for example, or ads. Not sure these scripts will work correctly if append them on the fly to required element.

    Btw, I’ve writen online tool to check how fast your site loads in browsers with page dependencies recursively – Site-Perf.com
    Maybe you’ll find it useful.
    It doesn’t apply script loading blocking – and now your article adds extra work to me :)

  17. Luke Chambers said:
    July 24, 2008 at 1:02 pm

    Does onDOMReady still fire after the js is loaded if you load it using this method?

  18. ArcehtypeJS uses both dynamic script and dynamic css loading (event html can be handled that way).

    It’s quite efficient and we are very happy with this system :)

  19. I worry about using multiple hostnames for images; that would require an extra DNS lookup per name (at least on initial page load) and those tend to be rather expensive.

  20. @Luke: DOMContentLoaded will fire before the JS, the experiment files are here: http://www.jspatterns.com/javascript-includes-and-domcontentloaded-event/

    @Glen: this is a valid concern. Yahoo’s best practices suggest max 2-4 domains per page, the research is linked to from this article.

  21. You state that the IE-only technique which uses the defer attribute of the script tag is unreliable – can you please elaborate? The attribute seems to fix onDOMReady problems in IE.

  22. Do you have a pointer to more information about the defer attribute of the script tag being unreliable in IE?

  23. Simon, Andrew, what I meant about defer is that using this attribute is not a guarantee that the script will wait, it depends on what other components are on the page. I’ve experimented in the past and found that for example if you have a non-deferred script that’s taking a while, the deferred might be executed meanwhile.

  24. [...] the YUI blog had a great post on how javascript blocks downloads and makes the page take longer to load. This is because any JS file can modify the DOM and so the [...]

  25. Nicely presented but definately not the first time I’ve seen this technique…. Lots of JS libraries have similar functionality, some for several years now.

    With all due respect perhaps you should check for existing solutions first and then spend your free time fixing problems that have no good solution as yet?

  26. [...] Stefanov en parle lui aussi, en [...]

  27. Take it to the extreme!!! Load an empty html, start imediately loading js (break it in several files), css AND the remaining html, all in parallel ( you can load more than 2 in parallel, using subdomains, the limit is 2/url, studies show that optimal is 4(subdomains)x2 ).

  28. how do i install javascript on my computer

  29. [...] Stoyan Stefanov is a member of Yahoo’s Exceptional Performance team; he’s worked on a variety of performance-related projects at Yahoo, including the popular YSlow plugin for Firebug. He’s also a contributing author here on YUIBlog. [...]

  30. Stoyan, I noticed the code on the 2nd example having some kind of loop. My javascript knowledge is tiny – is this some kind of way of loading multiple js files that’s not covered above? It almost looks like it could loop through a few js files…

  31. Hi Russell, it’s the YUI GET utility that’s doing the loop, you only need to call it passing an array:

    YAHOO.util.Get.script(urls);

    Here urls is an array. You may be confused by the bracket notation used to define an array in JavaScript (called “array literal notation”).

    This:
    var myarray = [1,2,3,4];
    is the same as:

    var myarray = new Array();
    myarray[0] = 1;
    //...
    myarray[3] = 4;

  32. Stoyan – I’ve seen an interesting limitation to the non-blocking JS technique. It turns out that in FF 3+, the “attach a script to the head” technique is only non-blocking until you run into another script tag from within the HTML – even if it only contains inline javascript. See the following example.

    var js = document.createElement('script');
    script.src = "something.js";
    var head = document.getElementsByTagName('head')[0];
    head.appendChild(js);

    the external script(s) will load in parallel with images 1-3, but FF will block once you reach the empty script tag until the external scripts are done loading.

  33. Gah – html got eaten -> trying again

    Stoyan – I’ve seen an interesting limitation to the non-blocking JS technique. It turns out that in FF 3+, the “attach a script to the head” technique is only non-blocking until you run into another script tag from within the HTML – even if it only contains inline javascript. See the following example.

    [head]
    [script]
    var js = document.createElement('script');
    script.src = "something.js";
    var head = document.getElementsByTagName('head')[0];
    head.appendChild(js);
    [/script]
    [\head]
    [body]
    [img src="A.gif?1"]
    [img src="A.gif?2"]
    [img src="A.gif?3"]
    [script][/script]
    [img src="A.gif?4"]
    [img src="A.gif?5"]
    [img src="A.gif?6"]

    [/body]

    the external script(s) will load in parallel with images 1-3, but FF will block once you reach the empty script tag until the external scripts are done loading.

  34. var js = document.createElement(‘script’);
    script.src = “something.js”;
    var head = document.getElementsByTagName(‘head’)[0];
    head.appendChild(js);

    Does this technique work with IE8 ? I’ve had success using this for some time with FF, Chrome and IE6 but onload event is not firing for IE8…

    The request is sent and valid data comes back – including a document.domain = ‘…’

    Any special considerations for IE8 ?