Loading JavaScript without blocking
I was reading Steve Souder’s blog post on loading scripts without blocking in which he notes that dynamically creating a <script> element and assigning its src attribute leads to a download that doesn’t block other downloads or page processes. His post is missing an example of how to do this, so I thought I’d pick up from there. I think most developers tend to use JavaScript libraries for such behavior (the YUI Get utility comes to mind) but a discussion of the underlying technique is still useful to know.
The basic approach to downloading JavaScript without blocking is quite straightforward:
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "file.js";
document.body.appendChild(script);
This is about as easy as it gets, you just create a new DOM element, assign its properties and add it to the page. There are two things to note about this code. First, the download doesn’t actually begin until the script node is added to the document. This is different from dynamically creating an <img> element, for which assigning the src automatically begins the download even before the node is added to the document. The second thing to note is that you can add the script node either to the <head> or <body>; it really doesn’t matter. That’s all it takes to dynamically load a JavaScript file without blocking the page.
Of course, you may also want to be notified when the JavaScript file is fully downloaded and executed, and that’s where things get a bit tricky. Most modern browsers (Firefox, Safari, Opera, Chrome) support a load event on <script> elements. This is an easy way to determine if the script is loaded:
//Firefox, Safari, Chrome, and Opera
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "file.js";
script.onload = function(){
alert("Script is ready!");
};
document.body.appendChild(script);
The real problem is in Internet Explorer, which uses the readyState property to indicate the state of the script and a readystatechange event to indicate when that property has changed. In this case, readyState isn’t a number as it is with the XMLHttpRequest object; instead, it’s one of five possible values:
- “uninitialized” – the default state.
- “loading” – download has begun.
- “loaded” – download has completed.
- “interactive” – data is completely available but isn’t fully available.
- “complete” – all data is ready to be used.
Even though the MSDN documentation indicates that these are the available values for readyState, in reality, you’ll never see all of them. The documentation also applies to other elements that also support readyState and leaves us hanging with a rather cryptic description of which readyState values to expect:
An object’s state is initially set to
uninitialized, and then toloading. When data loading is complete, the state of the link object passes through theloadedandinteractivestates to reach thecompletestate.
The states through which an object passes are determined by that object; an object can skip certain states (for example,interactive) if the state does not apply to that object.
Even stranger is that the final readyState isn’t always complete. Sometimes, readyState stops at loaded without going on to complete and sometimes it skips over loaded altogether. The best approach is to check for both readyState values and remove the event handler in both cases to ensure you don’t handle the loading twice:
//Internet Explorer only
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "file.js";
script.onreadystatechange = function(){
if (script.readyState == "loaded" ||
script.readyState == "complete"){
script.onreadystatechange = null;
alert("Script is ready!");
}
};
document.body.appendChild(script);
You can wrap these two approaches pretty easily to create a cross-browser function to dynamically load JavaScript:
function loadScript(url, callback){
var script = document.createElement("script")
script.type = "text/javascript";
if (script.readyState){ //IE
script.onreadystatechange = function(){
if (script.readyState == "loaded" ||
script.readyState == "complete"){
script.onreadystatechange = null;
callback();
}
};
} else { //Others
script.onload = function(){
callback();
};
}
script.src = url;
document.body.appendChild(script);
}
To use this, just pass in the URL to retrieve and a function to call once it’s loaded:
loadScript("http://yui.yahooapis.com/2.7.0/build/yahoo/yahoo-min.js",
function(){
YAHOO.namespace("mystuff");
//more...
});
Loading scripts in this way prevents them from blocking the download of other resources on the page or preventing the display from rendering. It’s a really useful technique when performance is important (and let’s face it, when is it never?). The really cool thing is that YUI 3 is built completely around the idea of non-blocking JavaScript downloads. All you need to do is download the ~20KB seed file and then specify the additional resources you want to load, such as:
YUI().use("dom", function(Y){
Y.DOM.addClass(document.body, "active");
});
Behind the scenes, YUI constructs the appropriate URL for the dom module and downloads it, automatically executing the callback function when the code is ready. This can really improve the initial download time of an overall page by asynchronously downloading the rest of the JavaScript code.
Loading JavaScript without blocking is a really important technique to understand and use in web applications that are concerned with page load performance. JavaScript blocking slows down the entire user experience, but it no longer has to.
Disclaimer: Any viewpoints and opinions expressed in this article are those of Nicholas C. Zakas and do not, in any way, reflect those of Yahoo!, Wrox Publishing, O'Reilly Publishing, or anyone else. I speak only for myself, not for them.
You can leave a response, or trackback from your own site.



14 Comments
Excellent example, but I have one question; the comparison table on Steve Souder’s page mentions that this method (adding a script tag) only ensures order in Firefox and Opera. I would think nested callbacks could solve this problem, only appending the next tag in the callback of the previous load, but that would no longer give parallel downloads.
Do you know the best way to keep parallel downloads but ensure script order?
HB on June 23rd, 2009 at 2:13 pm
Another great and succinct article, Nicholas. This seems to be a hot topic lately in the JavaScript community.
Joe McCann on June 24th, 2009 at 11:11 am
Asynchronously loading scripts is an interesting idea, but you have to be careful not to overdo it. Recently, we used the OpenLayers library (an open-source mapping gizmo), which uses this concept to dynamically load its components. Problem is, the number of scripts exceeds 200, all of them loaded almost at once- and you can clearly imagine what impact does it have on the browser performance (hint: crawl). What we did was substitute this 200 js files with one (concatenated, minified), which sped things up by a considerable factor.
Leszek Leszczynski on June 25th, 2009 at 5:09 pm
@Leszek - You’re absolutely correct. Loading 200 scripts without blocking the UI means that you’re holding up HTTP traffic while the browser queues up and sends each of the requests. Your suggestion to concatenate and minify is the best approach, and the one that YUI uses to load more resources on-demand.
Nicholas C. Zakas on June 27th, 2009 at 1:13 pm
[...] scripts without blocking by Steve Souder Loading JavaScript without blocking by Nicholas C. Zakas Even Faster Web Sites: Performance Best Practices for Web [...]
使用异步加载JavaScript文件来提高网页性能 | Jimbor's wORLD on June 28th, 2009 at 2:41 am
@HB - I personally think that if you’re loading a bunch of scripts dynamically, you’re probably doing something wrong. I prefer to concatenate all the files I need into a single response rather than worrying about maintaining the order of multiple scripts I want to download. That being said, it is possible to nest these calls to preserve order if necessary (but I wouldn’t recommend it).
Nicholas C. Zakas on July 1st, 2009 at 12:27 am
[...] too long ago, I wrote about loading JavaScript without blocking by creating a dynamic <script> tag. When <script> tags are in the flow of an HTML [...]
The best way to load external JavaScript | NCZOnline on July 28th, 2009 at 9:00 am
Hi,
Many times we also faced problems because of Google analytics script in our pages.
But how can I test this code?
To test this, I need a javascipt which actually loads very slowly (If I include a javascript then I want my page to get loaded after couple of 10s of seconds). How can I achieve this?
Kiran on September 4th, 2009 at 8:04 am
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 ?
Madhu Jahagirdar on September 18th, 2009 at 1:08 am
@Madhu - This is a side effect of how the DOM works. When you dynamically create elements that require outside JavaScript, there’s no way that the JavaScript could potentially change the structure of the page in such a way that the rendering should wait. This makes it safe to download and execute the code in a non-blocking manner.
Nicholas C. Zakas on September 18th, 2009 at 10:29 pm
Thanks for you response, however I have one more question related to it. I have read in your blogs and Steve Souders blog 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 ?
Madhu Jahagirdar on September 19th, 2009 at 12:53 pm
@Madhu - JavaScript is single-threaded and that means only one script can be executing a time. What we’re talking about is not JavaScript executing but rather JavaScript being downloaded. It doesn’t matter what order JavaScript is downloaded in so long as it is executed sequentially. When creating a dynamic script tag, the code is handed to the JavaScript engine for execution, but that queuing doesn’t block the page like it would if the script tag were embedded in the page statically.
Nicholas C. Zakas on September 19th, 2009 at 6:29 pm
This is a great post. Not having been in the Web Development field for very long, it came as quite a shock to find out the embedded script tags were blocking content download. In general, the most difficult aspects are managing scripts that have all ready been loaded and detection of the onload event. My attempts at implementing a script manager lead me to the following cross-browser issues that need to be managed.
Firefox guarantees the scripts will be executed in the order in which they were attached. If you insert scripts A and B in that order they will download asynchronously; however, script A must FINISH before script B can execute. So, if script A is lagged and script B finished quickly, it will not execute (nor will its onload event fire) until A has completed. This is in contrast to what I have seen in Safari, IE and Chrome.
In WEbKit, if the script’s src is in cache the onload event will fire as soon as the src is set. That means that, in your example, if file.js is in the browser cache, the onload event will never fire. I have found that this requires you to do two things: Setting the src attribute only AFTER the script is inserted into the DOM and subscribing to the onload event prior to DOM insertion. (This is definitely better than old versions of Safari which did not support the onload event on script elements). IMHO, IE’s readystate is more robust than a single onload event. Having this attribute available in Safari would certainly solve the problem.
YUI’s get() method only supports synchronized download of scripts. So, telling YUI to get Script A and Script B will download and execute Script A and only start on Script B when it receives A’s onload event. This definitely makes loading external JS files that need another library while they initialize, but it does not lead to an decrease in download times. A feature that seems to be unique to IE is that it will begin downloading an element as soon as the SRC attribute is set. That means that creating scriptEl and then doing scriptEl.src=”file.js” will begin the download of file.js. The script will not be executed, however, until you attach it to the document itself. This is similar to how all browsers handle the src attribute on an image and I think makes sense.
You can use the fact that FF guarantees execution order and that IE downloads as soon as the src attribute is set to achieve asynchronous downloads but synchronized execution. If Script B needs functions defined in Script A while it executes (not within functions defined in Script B) then B cannot run until A is complete. So, in FF you can simply create and attach script A followed by Script B. In IE, however, you create Script A and Script B at the same time. Then attach Script B after script A’s readystate is complete. Script B will change readystate almost immediately since it has all ready been fetched (In fact, Script B’s onreadystate function will fire before control is returned to the attaching function so watch out)
A couple of other idiosyncrasies:
1) When setting innerHTML to a value that contains a scirpt element, it is well known IE will not execute that script (FF will); however, what’s not as well known is that IE will still __DOWNLOAD__ any scirpt with a src attribute. If you use innerHTML to extract script tags in an AJAX routine or something, watch out for this especially if your remote scripts are uncacheable (we have the problem with AJAX content that contains ad banner scripts since the server sets explicit no-cache headers).
2) If you’re using a timeout handler of some kind to detect when a remote server is unavailable, you can remove the script element which will cancel the download, preventing script execution; however, FireFox does not support this. Couple this with FF’s need to execute scripts in insertion order and you have no way to handle a long-loading script from hanging the rest of the script elements (and thereby preventing your page from being functional)
ServerHerder on September 28th, 2009 at 3:00 pm
problem here is that the loading of the src url can block window.onload(). imagine that the target for the src attribute is a host that resolves but is not responding. while waiting for the timeout to expire anything registered with onload will be blocked. got any idea on how to work around that?
schelcj on October 7th, 2009 at 2:35 pm
Leave a Comment