Thursday, July 19, 2012

waitFor JavaScript

In the spirit of building the simplest thing that could possibly work, I revisit the concept in another of my posts: simple asynchronous JavaScript.

Time and time again I see web developers relying on the order that JavaScript loads in a page to ensure that the site will work. This keeps the mental model simple, "all the stuff above this code will be available here", but it means that people trying to speed up browsers and JavaScript interpreters have their hands tied. Why not instead load and execute as much JavaScript in parallel as possible, like a mutlithreaded application, and wait for your dependencies to load or preconditions to be met before your code runs.

You could do it all in just a few lines:

function waitFor(condition, callback) {
  function waiter(condition, callback) {
    return function() {
      var condMet = false;
      try {
        condMet = condition();
      } catch (e) {}

      if (condMet) {
        callback();
      } else {
        setTimeout(waiter(condition, callback), 5);
      }
    };
  }

  waiter(condition, callback)();
}

Using it looks a bit like this

waitFor(
  // condition
  function() {return window.dependency;},
  // callback
  function() {
    // do things using dependency.
  }
);

For example, here's how you might set this up to check if jQuery has been loaded and that a particular element on the page has been loaded in the DOM.

waitFor(function() {return $('#msgid');}, function() {
  $(document).ready(function(){
    $('#msgid').html('jQuery says "hi"!');
  });
});

Now it doesn't matter what order your script tags are in. Of course, you'll get the best performance if jQuery gets loaded before this waitFor call is made. The wait and retry loop adds some cost. If browsers had an event model available for script loading or conditions then we wouldn't have to poll using setTimeout/Interval. But for existing browsers all the way back to IE 6, this simple helper function will work.

For managing JavaScript dependencies there's also require.js which does quite a bit more (and is larger). I wrote this to be lighter weight and it is more general and flexible than a framework like require.js. Using waitFor, you could set up logic that is triggered if and when certain arbitrary conditions become true.

One little improvement, if you are waiting for lots of things then you would probably want to replace multiple timers with a single timer that will check all conditions. Like this:

function waitFor(condition, callback) {
  waitFor.waitingFor.push([condition, callback]);
  waitFor.check();
}

waitFor.waitingFor = [];

waitFor.check = function() {
  var stillWaitingFor = [];
  for (var i = 0; i < waitFor.waitingFor.length; i++) {
    var condMet = false;
    try {
      condMet = waitFor.waitingFor[i][0]();
    } catch (e) {}
    if (condMet) {
      waitFor.waitingFor[i][1]();
    } else {
      stillWaitingFor.push(waitFor.waitingFor[i]);
    }
  }

  waitFor.waitingFor = stillWaitingFor;
  if (stillWaitingFor.length > 0) {
    setTimeout(waitFor.check, 5);
  }
};

Here's a minified version. I added some newlines for formatting which you can take out.

function w(c,b){w.w.push([c,b]);w.c()}
w.w=[];w.c=function(){for(var s=[],b=0;b<w.w.length;b++)
{var d=!1;try{d=w.w[b][0]()}catch(e){}
if(d)w.w[b][1]();else s.push(w.w[b])}w.w=s;0<s.length
&&setTimeout(w.c,5)};var waitFor=w;

Happy coding!

Friday, May 11, 2012

JavaScript Caching: appendChild vs document.write

I found myself trying to prefetch some data which would appear in an iframe. I wanted to see if requesting the same JSON in the parent page and then in the iframe would cause the second request, made in the iframe, to hit the browser's cache in all cases. It turns out that for Internet Explorer and Web Kit based browsers it depends on how you request the JSON in the parent page.

If you use appendChild in IE the identical JS request will hit the browser cache.
If you use document.write in IE it will not hit the cache.

However, in Chrome and Safari if you appendChild in the parent, you will miss the cache. Using document.write in the parent page causes a cache hit.

Here are the results I gathered.
Cache Hit? both document.write parent document.write
iframe appendChild
parent appendChild
iframe document.write
both appendChild
Internet Explorer miss miss hit hit
Firefox hit hit hit hit
Chrome hit hit miss miss
Safari hit hit miss miss
Opera hit hit hit hit
Android ? ? ? ?
iOS ? ? ? ?

All versions of IE behaved the same, I didn't check different versions of the other browsers.

To test this I wrote a test web application called json-caching-exploration. It's a simple web app that runs on App Engine.

Server and HTML
JavaScript

I haven't run this against mobile browsers yet. If you give it a try please let me know what you find.

It's a shame that these browsers behave differently, especially given the differences in browser behavior between loading JavaScript using document.write and appendChild.