With AJAX becoming more and more common place on the web, old lessons need to be re-leant in order to create user interfaces that don’t hang.

Anyone with any AJAX experience will be familiar with the problem. You fire off your AJAX request, you get your response, you update the page and everything is good… except it isn’t. Updating the page takes forever and while that’s happening the user can’t do anything, not scroll, navigate to another page or even close the current page.

The problem occurs because the browser (at least in the case IE & Firefox) have a single thread for processing events, be they mouse clicks, key presses, time-outs or AJAX responses. This means that if you have some Javascript code that’s taking a while to process, then no other events will be processed until after it’s finished. It would be nice to point the finger and blame the browser (at least that’s what we usually do for IE ;)), but they are actually doing what they are supposed to do. They are supposed to process these events things serially (well there is one little exception. If the event is a mouse click or key press that falls outside of the web page, then for the love of god, handle is separately. Yes, the page should lock up, but not the entire browser. Grrrrr…).

Unsurprisingly, this isn’t the first time we have encountered this problem in the programming world. Non-web applications have the same problem; if you block the main thread the whole application hangs. So how do they solve the problem for non-web apps? Well, there are two common techniques:

  1. Do the resource intensive work in another thread.
  2. Break the background function into units of work and interleave those units with the UI processing.

We can rule out threads straight away because Javascript (in the context of a browser) doesn’t have them and is not likely to support them any time soon. Fortunately the second technique is possible and not particularly hard in Javascript.

The first step is to modify your functions so that can be broken down into unit of works. We do this by making them recursive (who said recursion was bad?). Here is an example:
Before:

function processSomeData1(target, someData) {
  for (i = 0; i < someData.length; ++i) {
    processAChange(target, someData[i]);
  }
}

Note: The following example will cause your browser to hang until it finishes running. This takes less than 2s on my machine, but could take much longer on less powerful machines.

After:

function processSomeData2(target, someData, i) {
  if (i == undefined) {
    i = 0;
  }
  if (i < someData.length) {
    processAChange(target, someData[i]);
    processSomeData2(target, someData, i + 1);
  }
}

Note: The following example will cause your browser to hang until it finishes running. This takes less than 4s on my machine, but could take much longer on less powerful machines.

Now run your unit tests (yes, you can unit test Javascript) and make sure you didn’t break anything. Even if you did everything right you may still get a “too much recursion” or “Out of stack space” error as in example 2 above. This isn’t going to be a problem later on, but for the moment you can using batching to fix it and then later you can use batching to tune the performance. Here’s the batching version:

function processSomeData3(target, someData, i) {
  if (i == undefined) {
    i = 0;
  }
  if (i < someData.length) {
    var batchSize = 100;
    for (j = i; j < i + batchSize && j < someData.length; ++j) {
      processAChange(target, someData[j]);
    }
    processSomeData3(target, someData, i + batchSize);
  }
}

The next step is to replace the recursive call with a setTimeout() call. By using setTimeout() with a time-out of zero, we are telling the browser to execute the next unit of work as soon as it has finished processing any pending events. This is what allows the browser to update the page and respond to user input while our function is executing. Here’s what the new code looks like:

function processSomeData4(target, someData, i) {
  if (i == undefined) {
    i = 0;
  }
  if (i < someData.length) {
    var batchSize = 100;
    for (j = i; j < i + batchSize && j < someData.length; ++j) {
      processAChange(target, someData[j]);
    }
    var nextBitOfWork = function () {
        processSomeData4(target, someData, i + batchSize)
      };
    setTimeout(nextBitOfWork, 0);
  }
}

It would be nice to think that we are all done and dusted, but life is never that easy. Have a look what happens when you run the above example twice in quick succession. If you look carefully, you’ll notice that the results from both runs are interleaved. Bummer. A simple solution would be to disable the execute button (and the clear button) until the run completes, but this is not suitable in all cases; sometimes you want the user to be able to cancel the current run or start a new run with different input.

If the AJAX request is still in flight, then we can just abort the request using abort() function (You see heaps of sites that present an AJAX loading bar while the request is in flight or while the response is being processed, but you almost never see it accompanied with a stop button. That’s lazy. Make a stop button part of your loading bar and make it work). If our function is being executed, then we just need to cancel the last time-out. To make our lives easier, I’m going to use the following class

function Backgrounder(initialFunc) {
  this.timeout = null;
  this.schedule = function (func) {
    var self = this;
    this.timeout = setTimeout(function () {
      self.execute(func);
    }, 0);
  }
  this.execute = function (func) {
    this.timeout = null;
    if (!this.aborted) {
      func();
    }
  }
  this.interrupt = function () {
    clearTimeout(this.timeout);
    this.timeout = null;
  }
  this.finished = function () {
    this.abort();
  }
  if (initialFunc != undefined) {
    initialFunc(this);
  }
}

Backgrounder calls setTimeout() for us, keeps track of the last time-out ID and allows us to interrupt() the execution of our function. We can use Backgrounder as a replacement for setTimeout() as follows:

function processSomeData5(backgrounder, target, someData, i) {
  if (i == undefined) {
    i = 0;
  }
  if (i & lt; someData.length) {
    var batchSize = 100;
    for (j = i; j < i + batchSize && j < someData.length; ++j) {
      processAChange(target, someData[j]);
    }
    var nextBitOfWork = function () {
        processSomeData5(backgrounder, 
                         target, 
                         someData,
                         i + batchSize)
      };
    backgrounder.schedule(nextBitOfWork);
  }
}

or we can subclass from it:

function ProcessSomeData6(target, someData) {
  this.inherits_from = Backgrounder;
  this.inherits_from();
  this.target = target;
  this.someData = someData;
  this.batchSize = 100;
  this.process = function (i) {
    if (i & lt; this.someData.length) {
      for (j = i; j < 
           i + this.batchSize && j < this.someData.length;
           ++j) {
        processAChange(this.target, this.someData[j]);
      }
      var self = this;
      var nextBitOfWork = function () {
          self.process(i + self.batchSize)
        };
      this.schedule(nextBitOfWork);
    }
  }
  this.process(0);
}

What you have just seen is a very poor man’s version of continuations. Continuations are a language construct that allows a function to save it’s current state, return and later on resume from the saved state and location.

There is a version of Javascript called Narrative Javascriptthat does support proper continuations (rather than the poor man’s version above). Narrative Javascript uses a new “yielding” operator (‘->’) that allows you to write code that would normally block in a linear code block, rather than the mess we saw above. The best thing about Narrative Javascript though, is that it is converted into normal Javascript and can run in a standard browser. Here is the same example, but this time written in Narrative Javascript(I’ve kept the batching code in there, because as before we can adjust the batch size to tune the performance):

function yield(notifier) {
  setTimeout(notifier, 0);
  try {
    notifier.wait->();
    return true;
  } catch (e
  if e == NJS_INTERRUPT) {
    return false;
  }
}
function processSomeData7(target, someData, notifier) {
  var batchSize = 100;
  for (i = 0; i < someData.length; i += batchSize) {
    for (j = i; j < i + batchSize && j < someData.length; ++j) {
      processAChange(target, someData[j]);
    }
    if (!yield->(notifier)) return;
  }
}

Note: Example seven isn’t working under IE.
Very nice! Now before you run off and try to convert all your Javascript code to Narrative Javascript, be warned that it’s still in beta, and it doesn’t play nice with classes and inheritance. I might have been doing something wrong, but the Narrative Javascript Documentation is quiet brief, so it’s hard to tell. That being said, it’s obviously got huge potential.

Conclusion

As you have seen above, it’s not too difficult to modify your Javascript so that your Web App remains responsive. It’s just a matter of locating the functions that need to be processed in the background, modifying the code so it can yield, and then tuning the amount of work that can be done in a single hit.

Feel free to grab the code for these examples below and have a play around with it yourself.

Creative Commons License
This work is licensed under a Creative Commons Attribution 2.5 License.