Async JavaScript callbacks

June 3rd, 2012. Tagged: IE, JavaScript

Ah, asynchronous JavaScripts. Love 'em, hate 'em, but you gotta use them!

I have quite a few posts here on this blog about this stuff. Starting with something I considered an interesting hack to emulate PHP's require(). This was posted in 2005. (2005! That's ancient. That's only a year after gmail was introduced, and you know gmail has been around, like, always). Then there was this and this and this and this. These days dynamic SCRIPT tags are something common, mainly because of their non-blocking behavior that helps performance. This is all good.

I wanted to highlight the part where you load a script file and then execute a function once the file is done loading. A common, but kinda wrong pattern I've posted back at the time, and tweaked a little since, is like so:

var first_js = document.getElementsByTagName('script')[0];
var js = document.createElement('script');
js.src = file;
 
// normal browsers
js.onload = function () {
  alert('loaded!!');
};
 
// IE
js.onreadystatechange = function () {
  if (js.readyState in {complete: 1, loaded: 1}) {
    alert('loaded!!');
  }
};
 
 
first_js.parentNode.insertBefore(js, first_js);

Back at the time (2006) this was fine. The problem is now that since version 9, IE supports onload handler in script elements. But it also supports onreadystatechange for backwards compatibility.

In other words in IE9+ your callbacks will be executed twice. Not good.

Single callback

There are various ways to deal with this situation.

1. You can delete the onload callback in readystatechange, beacuse readystatechange is called first.

js.onreadystatechange = function () {
  if (js.readyState in {complete: 1, loaded: 1}) {
    callback();
    js.onload = null;
  }
};

2. You can use a single assignment to both

js.onload = js.onreadystatechange = function () {
  // stuff...
  js.onload = js.onreadystatechange = null;
 
};

The problem with both of these is that readystatechange is involved even in browsers that are modern (IE9+) and support onload. Feels a bit ugh.

3. You can sniff onload support

if (typeof js.onload !== 'undefined') {
  // good stuff..
} else {
  // onreadystatechange jazz
}

This works because old IEs will not have any onload property (hence undefined) while supporting browsers will have this property initially set to null.

Hmm, making a distinction between two falsy values null and undefined seems a little fragile. The next developer will come and say: "meh, what's with the typeof verbosity, let's just say if (js.onload)"... And the whole thing will fail.

4. (And this is my preferred method) is to sniff support using addEventListener.

It just happens so that IE9, which supports onload, is also the first IE browser that supports addEventListener.

The whole thing looks like:

var first_js = document.getElementsByTagName('script')[0];
var js = document.createElement('script');
js.src = file;
 
if (js.addEventListener) { // normal browsers
  js.addEventListener('load', function(){
    alert('done!!');
  }, false);
} else {
  js.onreadystatechange = function() { // old IEs
    if (js.readyState in {loaded: 1, complete: 1}) {
      js.onreadystatechange = null;
      alert('done!!');
    }
  };
}
 
first_js.parentNode.insertBefore(js, first_js);

Drawback is that you decide on a feature (script onload support) based on a different feature (addEventListener support). I can live with this. We're talking here about an exception for known legacy browsers and shouldn't be an issue going forward in this brave new world where everyone lives in piece and love and brotherhood and sisterhood and motherhood and all browsers support onload and addEventListener.

So anyway, choose your poison :)

Here's a test page that listens to everything so you can play in different browsers:
http://www.phpied.com/files/jsasync/loaded.html

BTW, notice that IE is the only browser that fires window.onload before the onload of the (slow) script. This is another thing to keep in mind and look out for.

Tell your friends about this post: Facebook, Twitter, Google+

11 Responses

  1. Excellent writing.

  2. pretty smart the way you check “readystate” status

  3. Great summary, Stoyan. I use option #2 and feel okay about it.

    Your point about whether or not browsers block window.onload when loading async scripts is important. According to the Browserscope user test I ran (see http://www.stevesouders.com/blog/2012/01/13/javascript-performance/) it’s more complex than just IE. Browsers that do NOT block window.onload because of async scripts include IE 6-9, Android 2, iPhone 4, and Safari 5.0. But note that newer versions of these browsers (IE 10, Android 4, iPhone 5, Safari 5.1) DO block window.onload falling in line with most other browsers.

  4. Asynchronous scripts are blocking window on load event on Firefox, Safari and Chrome. By wrapping the function call with setTimeout with zero interval is unblocking the window on loading event. This trick is working in Chrome and Safari but not in Firefox.

  5. Gabriel Gilini

    It’s all fun and games until the new version of BrowserX comes out. And guess what, he doesn’t support onload but does support addEventListener. Then what? Scavenge all the code out there in production to change it all over again?

    Anyway, solving this “problem” is as easy as setting a flag to `true’ whenever any of the callbacks fire, and check said flag before executing it again.

    No sniffing or any type of weak inference needed.

    By the way, complaining about the “verbosity” of typeof is just stupid. Just write CoffeeScript or what have you.

  6. Here are the URLs to show case asynchronous calls blocking the window on load event and work around to unblock the window on load event. Please use Chrome or Safari.

    Page with Issue: http://nidhisekhar.com/poc/async_script_blocks_onload.html
    Page with solution: http://nidhisekhar.com/poc/async_script_blocks_onload_fix.html

  7. This is madness. If you have to refer to empirical test results to determine whether your strategy “works” in “current browsers”, you’ve thrown the game away.

    And script loaders are a complete waste of time anyway (not to mention hazardous to your documents).

    Think.

  8. @Steve Souders

    “Great summary, Stoyan. I use option #2 and feel okay about it.”

    You mean the “summary” that leads off with “Ah, asynchronous JavaScripts. Love ‘em, hate ‘em, but you gotta use them!” and then fails to explain that assertion. :(

    And why do you do that? Loading scripts out of order is neither practical nor useful and you don’t have to listen for when a script has loaded (just call back from your – provide – function). The require/provide “problem” was solved with 100% reliability and maximum performance three years ago (and did not use *any* of the techniques described here).

    If you are worried about blocking the parser, put the scripts at bottom of the body (and deal with FoUC of course). All of this other “asynchronous” crap is a solution desperately seeking a problem.

    HTH

  9. @Raja,

    If you’d like to care about blocking onload, you can set window.setTimeout(load, some_msec);. See http://jsfiddle.net/tokkonoPapa/ea7Qt/

    I don’t know how much the magic number ‘some_msec’ is appropriate, but I think it might be a similer reason as Steve Souders said: DHTML + setTimeout stylesheet or it concerns with something like content.notify.interval.

  10. You actually make it appear really easy with your presentation however I to find this matter to be actually something which I think I would never understand. It kind of feels too complex and very broad for me. I am looking forward to your next put up, I will try to get the cling of it!

  11. [...] with the setTimeout function etc. But I found the best solution to this asynchronious problem on phpied.com (thx a lot for your [...]

Leave a Reply