Loading Scripts Without Blocking

April 27, 2009 10:49 pm | 47 Comments

This post is based on a chapter from Even Faster Web Sites, the follow-up to High Performance Web Sites. Posts in this series include: chapters and contributing authors, Splitting the Initial Payload, Loading Scripts Without Blocking, Coupling Asynchronous Scripts, Positioning Inline Scripts, Sharding Dominant Domains, Flushing the Document Early, Using Iframes Sparingly, and Simplifying CSS Selectors.

As more and more sites evolve into “Web 2.0” apps, the amount of JavaScript increases. This is a performance concern because scripts have a negative impact on page performance. Mainstream browsers (i.e., IE 6 and 7)  block in two ways:

  • Resources in the page are blocked from downloading if they are below the script.
  • Elements are blocked from rendering if they are below the script.

The Scripts Block Downloads example demonstrates this. It contains two external scripts followed by an image, a stylesheet, and an iframe. The HTTP waterfall chart from loading this example in IE7 shows that the first script blocks all downloads, then the second script blocks all downloads, and finally the image, stylesheet, and iframe all download in parallel. Watching the page render, you’ll notice that the paragraph of text above the script renders immediately. However, the rest of the text in the HTML document is blocked from rendering until all the scripts are done loading.

Scripts block downloads in IE6&7, Firefox 2&3.0, Safari 3, Chrome 1, and Opera

Browsers are single threaded, so it’s understandable that while a script is executing the browser is unable to start other downloads. But there’s no reason that while the script is downloading the browser can’t start downloading other resources. And that’s exactly what newer browsers, including Internet Explorer 8, Safari 4, and Chrome 2, have done. The HTTP waterfall chart for the Scripts Block Downloads example in IE8 shows the scripts do indeed download in parallel, and the stylesheet is included in that parallel download. But the image and iframe are still blocked. Safari 4 and Chrome 2 behave in a similar way. Parallel downloading improves, but is still not as much as it could be.

Scripts still block, even in IE8, Safari 4, and Chrome 2

Fortunately, there are ways to get scripts to download without blocking any other resources in the page, even in older browsers. Unfortunately, it’s up to the web developer to do the heavy lifting.

There are six main techniques for downloading scripts without blocking:

  • XHR Eval – Download the script via XHR and eval() the responseText.
  • XHR Injection – Download the script via XHR and inject it into the page by creating a script element and setting its text property to the responseText.
  • Script in Iframe – Wrap your script in an HTML page and download it as an iframe.
  • Script DOM Element – Create a script element and set its src property to the script’s URL.
  • Script Defer – Add the script tag’s defer attribute. This used to only work in IE, but is now in Firefox 3.1.
  • document.write Script Tag – Write the <script src=""> HTML into the page using document.write. This only loads script without blocking in IE.

You can see an example of each technique using Cuzillion. It turns out that these techniques have several important differences, as shown in the following table. Most of them provide parallel downloads, although Script Defer and document.write Script Tag are mixed. Some of the techniques can’t be used on cross-site scripts, and some require slight modifications to your existing scripts to get them to work. An area of differentiation that’s not widely discussed is whether the technique triggers the browser’s busy indicators (status bar, progress bar, tab icon, and cursor). If you’re loading multiple scripts that depend on each other, you’ll need a technique that preserves execution order.

Technique Parallel Downloads Domains can Differ Existing Scripts Busy Indicators Ensures Order Size (bytes)
XHR Eval IE, FF, Saf, Chr, Op no no Saf, Chr ~500
XHR Injection IE, FF, Saf, Chr, Op no yes Saf, Chr ~500
Script in Iframe IE, FF, Saf, Chr, Op no no IE, FF, Saf, Chr ~50
Script DOM Element IE, FF, Saf, Chr, Op yes yes FF, Saf, Chr FF, Op ~200
Script Defer IE, Saf4, Chr2, FF3.1 yes yes IE, FF, Saf, Chr, Op IE, FF, Saf, Chr, Op ~50
document.write Script Tag IE, Saf4, Chr2, Op yes yes IE, FF, Saf, Chr, Op IE, FF, Saf, Chr, Op ~100

The question is: Which is the best technique? The optimal technique depends on your situation. This decision tree should be used as a guide. It’s not as complex as it looks. Only three variables determine the outcome: is the script on the same domain as the main page, is it necessary to preserve execution order, and should the busy indicators be triggered.

Decision tree for optimal async script loading technique

Ideally, the logic in this decision tree would be encapsulated in popular HTML templating languages (PHP, Python, Perl, etc.) so that the web developer could just call a function and be assured that their script gets loaded using the optimal technique.

In many situations, the Script DOM Element is a good choice. It works in all browsers, doesn’t have any cross-site scripting restrictions, is fairly simple to implement, and is well understood. The one catch is that it doesn’t preserve execution order across all browsers. If you have multiple scripts that depend on each other, you’ll need to concatenate them or use a different technique. If you have an inline script that depends on the external script, you’ll need to synchronize them. I call this “coupling” and present several ways to do this in Coupling Asynchronous Scripts.

47 Responses to Loading Scripts Without Blocking

  1. Great article Steve,

    the table is very handy.

    Perhaps it’s good to add a small note to the the first waterfall chart, about the image, stylesheet, and iframe being served from more than one domain. This explains why they all download in parallel on IE7…

    Cu at the Fronteers conf in Amsterdam end of year …

    – Aaron

  2. Steve,

    I had seen your videos and slideshows from the SXSW talks, and wrote up a quick technique to pull off parallel loading based on what you had said. You can find the technique at http://piecesofrakesh.blogspot.com/2009/03/downloading-javascript-files-in.html on my blog. The approach I took fits into the different domains > preserve order.

    Would love your feedback about it.

  3. Great article, nice summary of the options when using straight-up JavaScript.

    One comment, for what it’s worth: the Dojo toolkit provides a form of XHR script injection that can be both cross-domain, and order-preserving. I suspect some of the other JS frameworks and libraries have similar mechanisms as well, but they’re all additions to JS and not part of its core functionality, so may not be for everyone.

  4. @Ryan: You might want to confirm if the XHR script injection you’re referring to downloads scripts in parallel. I believe Dojo’s “require” functionality doesn’t load scripts in parallel (that’s how they preserve order).

  5. Great article Steave!

    I think that is possible to use XHR Eval or XHR Injection techniques on a cross-site situation. Using JSONP argument in the url request, the browser doesn’t block the new script .
    However, the external script have to be dinamic (PHP, Python, jsp, etc)

    Cheers from argentina

  6. Note that CPU is another important metric for website performance. In particular, I’d be worried about using XHR eval and XHR injection, given this quote from Flickr’s blog (http://code.flickr.com/blog/2009/03/18/building-fast-client-side-searches/):

    “While all of our tests ran to completion (even the 10,000 contacts case), parse time per contact was not the same for each case; it geometrically increased as we increased the number of contacts, up to the point where the 10,000 contact case took over 80 seconds to parse — 400 times slower than our goal of 200ms. It seemed that JavaScript had a problem manipulating and eval()ing very large strings, so this approach wasn’t going to work either.”

    If you go down this route, you might want to do a little JS profiling while you’re at it!

  7. “Browsers are single threaded”

    I always say: Browser-windows are single threaded. And I do mean the browser-object: window. So an iframe or browser tab is an other window for example. If you call a function in the main page from an iframe and the main page is busy doing something else, it will block the iframe until the main page is done.

  8. Does caching work with all the mentioned methods?

  9. I think that it’s possible an other technique. If you define a “script” tag and set it an ID. Then you can do this to set the script:

    document.getElementById(ID).setAttribute(‘src’, script-url)

    I’m using this technique to prevent adsense blocking, and it’s working!

  10. Hello, nice article. But what exactly do you mean by managed XHR injection?

  11. “Managed” means there’s additional code that wraps the XHR that does things like ensuring execution order and triggering busy indicators (if desired). The chapter in the book contains a full code example.

  12. Awesome article… I am a big fan of your web performance tips, they are very effective and simple to implement.

    Thanks

  13. Thanks for this article. You solved me a great problem!

  14. Thanks Steve. In the chart you say that FF “ensures order” for the Script DOM Element technique. Is this true for FF 1.x as well, or only more recent versions?

  15. @Scott: I only tested on FF2 & 3. If you have access to FF1, please try this test page and post the results. Are the scripts loaded in order (1.cuzillion, 2.cuzillion, 3.cuzillion)? If so, the order is preserved.

  16. Hi Steve,
    interesting research but I’m afraid you may be partly wrong about the performance benefits of using dynamic SCRIPT tags in Opera. I’m not sure how you define “parallel” loading here. If you mean that the dynamically added SCRIPT tags are loaded in parallel, this is probably true, but if you mean that they are loaded in parallel with regular SCRIPT tags in later markup it’s not correct. As seen while debugging the AOL problem here:
    http://my.opera.com/hallvors/blog/2009/03/07/websites-playing-timing-roulette
    Opera does not keep parsing the document while loading a SCRIPT tag that is added dynamically through the DOM. We’re going to fix that but not for Opera 10.

  17. @Hallvord: Thanks for the clarification. In my tests I use the advanced script loading technique for all scripts in the page. It is possible that a web site might do the extra work to use these advanced techniques, but not apply them to all the scripts in their page (although that might be unlikely). In which case, if they used the Script DOM Element approach (what you call “dynamic SCRIPT tags”), they wouldn’t get parallel loading for other scripts in the page (although all other resources would load in parallel). Note that all the other parallel loading techniques do work in Opera, even with other scripts.

  18. I may be seeing a downside in the above-referenced IE 8 ability to download scripts in parallel. I have seven external scripts in the that need to be loaded prior to the body’s onload event. Sometimes they are. Sometimes they’re not – leading to errors.

  19. Browsers are single threaded? I believe this is not correct – how else can they download in parallel. What is not possible is to execute javascript in parallel – which is addressed through web workers in html 5. I’d think the javascript engine is single threaded.

  20. Is there any technical reason why creating DOM element and downloading javascript does not block other downloads or its just that browsers behave that way ?

  21. I’ve never heard an explanation of why browsers work this way. Any browser developer want to comment?

  22. Thanks for you response, however I have one more question related to it. I have read in your blogs that when tag is encountered it is passed to javascript engine, which is single threaded, that downloads the scripts, parses and executes it, and thats the reason why when one script is being downloaded ( as javascript engine is single threaded) we cannot download next javascript.

    When we insert the script tag using DOM does it still pass it to javascript engine for downloading, parsing, and executing, if so, as it is single threaded should it not behave exactly same as using the tag ?

    Let me know if my interpretation and observation is correct ?

  23. @Madhu: Browsers are limited to parsing and executing one script at a time, but there’s no reason they can’t *download* multiple scripts in parallel. That’s exactly what has happened in new browsers. The techniques I talk about here trick older (and newer) browsers into doing a better job of downloading scripts in parallel with other resources, but the JS parsing and execution is still one-at-a-time.

  24. Steve, I have observed that when the JS is being parsed and executed browser can download components if they are in the cache and they dont block them, is my observation correct ?

  25. @Madhu: The browser won’t start any downloads or cache reads during JS parsing and execution. However, browsers will continue to accept responses for any downloads started *before* the JS parsing and execution.

    Here’s an example: https://stevesouders.com/cuzillion/?c0=bi1hfff3_0&c1=bb0hfff0_2

    This example contains an image that takes 3 seconds to download and an inline script that takes 2 seconds to execute. The browser starts downloading the image. While the image is downloading, the browser executes the 2 second JS. The overall page load time is 3 seconds, proving that the image is downloaded in parallel with the JS.

    Notice that if you switch the order ( https://stevesouders.com/cuzillion/?c0=bb0hfff0_2&c1=bi1hfff3_0 ), the browser doesn’t start the download until *after* the script is done, resulting in a 5 second page load time.

  26. Steve,
    First let me start by saying wonderful article.

    I have some additional input and suggestions that I’d like to share.

    There is a ton of external javascript out there that rely on document.write to inject html directly to the DOM, typically immediately below the executing scripts DOM element. Ad agencies are known for using this technique, as well as many widgets out there.

    Not every technique described in this article ensures proper placement of injected html.

    In particular this test page: http://www.getsnappy.com/blog/javascript-test/
    reveals that the Script DOM Element technique doesn’t ensure proper placement of injected html in all browsers.

    I suggest adding a technique “postload dummy replacement”, that I describe here: http://www.getsnappy.com/blog/web-optimization-page-load-time/dont-let-ads-widgets-and-other-external-javascript-slow-you-down/

    This technique uses a standard synchronous JavaScript call strategically placed at the bottom of the page inside an invisible dummy DIV. By placing the call at the bootom of the page you avoid blocking. After the JavaScript calls have executed, the dummy div will contain the injected html from the external javascript call. This contents will then be swapped with the contents of a placeholder DIV placed at the desired location.

    The downside to this technique is that if you want to include more than one script using this method, they are downloaded and executed serially.

  27. @Brian: Great comments. You’re right that these async techniques shouldn’t be used for JS that does document.write. I’ve seen people do the DIV-at-the-bottom technique. It works pretty well, but folks I’ve talked to have described issues, although I don’t have those cataloged anywhere.

  28. Steve,

    In Fig 2 above, why is it that the GIF and iframe are blocked while the stylesheet gets downloaded in parallel with the scripts. Has it got something to do with parallel connections?

  29. @Manjusha: It varies by browser. I’m not certain, but I would guess it has to do with their lookahead code – maybe it only looks for or prioritizes certain types of resources. It’s not parallel connections – these resources are on different domains.

  30. I may be seeing a downside in the above-referenced IE 8 ability to download scripts in parallel. I have seven external scripts in the that need to be loaded prior to the body’s onload event. Sometimes they are. Sometimes they’re not – leading to errors.

  31. @Matt: You can control whether scripts are loaded before the onload event. Using the normal SCRIPT SRC tag will always load script before the onload event. All of my advanced async loading techniques except for the XHR techniques load scripts before the onload event.

  32. Hello,

    as from now, can we consider the fact that some relatively “old” browsers can’t download scripts in parallel is a problem only for websites that have a lot of javascript files + a very large audience + badly positioned scripts ?

    Plus, the difficulty of using theses techniques can be very high

    In my daily work i found that dealing with scripts that uses document.write is a larger problem than this. And this was not the case some months ago (when FF3.0 and IE7 were leading the browser market)

    Sorry i’m french and it’s difficult to explain my thoughts.

    Summary : is this still a BIG problem for websites or just for particular cases ?

  33. @Vincent: This is still a big problem. IE6&7 represent a large percentage of users. Scripts in IE8 block image and iframe downloads. Opera doesn’t have any parallel downloading. These techniques are also useful for lazy-loading scripts.

  34. Brian & Steve (re: document.write()),

    In case you’re ever stuck with an external script that insists on using document.write, you can always modify the document.write method around the script. That is,

    var writeValue, tmpWrite = document.write;
    document.write = function(c) { writeValue = c; };
    //apply script DOM technique here, where document.write’s result is now stored in writeValue
    document.write = tmpWrite;

    Hackalicious, but successfully tested on multiple browsers.

    Cheers!

  35. Hi, am i crazy or IE8 do not download scripts in parallel by default ?

    this is the webpagetest for the example “script block download” you give :

    http://www.webpagetest.org/result/100303_5KS9/1/details/

    we can see that the two javascripts are not downloaded in parallel.

    I’ve also made test page with cuzillon :

    scripts in header :
    http://www.webpagetest.org/result/100303_5KS6/

    scripts in body :
    http://www.webpagetest.org/result/100303_5KRM/

    defer works (but if defer is used in header scripts, it only download 2 scripts in parallel)

  36. Ok nevermind, httpwatch basic shows that scripts are downloaded in parallel, i guess webpagetest is bugged.

    sorry

  37. @Steve

    Am I doing something wrong, because using Firebug/WebInspect when I use the example code below – yes, my JavaScript downloads in parallel but my images are still blocked from download until the JavaScript has completed.

    ##################
    var head = document.getElementsByTagName(“head”)[0];
    var sTag1 = document.createElement(“script”);
    sTag1.type = sTag1.type = “text/javascript”;
    sTag1.src = “one.js”;
    var sTag2 = document.createElement(“script”);
    sTag2.type = sTag2.type = “text/javascript”;
    sTag2.src = “two.js”;
    head.appendChild(sTag1);
    head.appendChild(sTag2);
    ##################

    Would you mind posting the code to accomplish your Script Dom Element example.

    Thanks

  38. @Tim: As I mentioned in the blog post, you can use Cuzillion to see all the techniques. One of the things I love about Cuzillion is that you can create test cases and share them easily. Here’s a test page that shows 2 external scripts and an image. In FF 3.6 they all download in parallel.

  39. Hi Steve,

    There seems to be a lot of disagreement on StackOverflow about asynch JavaScript downloading.

    http://stackoverflow.com/questions/2803305/javascript-how-to-download-js-asynchronously

    Would you mind looking into this post and commenting.

    You’re awesome. Many thanks.

  40. Hi Steve Souders,
    We are working on a web site which monitors the work flow of documents in an organization,
    In this project we use telerik controls,which affects the performance of our site because it(Controls) generates scripts while rendering,
    what should i do in order to achive high performance.

    Thanks in advance.

  41. @Ramakrishnan: If these are inline scripts, it’s less of an issue. If these are external scripts, you could modify how telerik works so that it inserts scripts using an async technique. If you have more questions, please contact me directly.

  42. Steve,

    I am not sure what exactly I am missing, but I can not seem to get order of script execution preserved. I have read this blog as well as your book “Even Faster Websites”. I looked at the example you gave for “FF Ensured Order Execution” and did see that your scripts were loaded in the order called. I replaced your default scripts with my own and I don’t seem to get order of execution preserved. I have a line at the top of each script that writes out the innerHTML of a div tag, and when I refresh the page, they will write out in random orders, no matter which order they are called to load in.

    Am I missing something, does “FF Ensured Order Execution” mean the scripts will be executed in the order they are loaded, so if one script is dependent on code in another script I can be sure the first called will be the first executed?

    Any insight would be greatly appreciated. I put the url where I placed the demo in the website input field.

  43. EFWS 13.3.2 stylesheet before iframe said that in IE and FF stylesheet before iframe would block ifame.but in your first waterfall chart “Scripts block downloads in IE6&7, Firefox 2&3.0, Safari 3, Chrome 1, and Opera”, the iframe and stylesheet are downloaded in parallel,why??

  44. @seaverbo: You have to read the entire page in EFWS. It says “In Firefox, the stylesheet and iframe download in parallel, but the iframe’s resources are blocked by the stylesheet.” That’s also stated again in the example from that page in EFWS.

  45. Hi Steve,

    I noticed that FF did not ensure order using “Script DOM Element” since its version 3.6.

    thx,
    Kail

  46. @Kail: Thanks for that update. It’s very significant and perhaps breaks some of my older examples. I know it broke other JS loaders, such as labjs.

  47. check out the technique I wrote, here:
    http://async-iframe.blogspot.com/2013/03/async-iframe-technique.html