2008-04-10

Expensive, yet responsive, live interaction

Let's say you have a slider widget that users can drag to filter a largeish table so it only shows rows where some column is greater than the a threshold, defined by the slider. That's an application with rather strict real-time constraints, but filtering the table is expensive; a naïve solution that refilters the table with every drag event you get, or even just every event that changes your threshold value, will still be too slow for the interface to feel responsive.

I just came up with a rather elegant solution for that problem, which makes sure that your expensive code won't make the interface feel sluggish, by postponing calls to it that come in a more rapid succession than the time it would have taken for it to run. Or, to be precise, the time it took last time it was called. It's a simple wrapper function you can reuse anywhere:

// returns a function that only runs expensive function
// fn after no call to it has been made for n ms, or
// 100, if not given, or the time fn took last time, if
// it has been run at least once and no n was given.
function expensive(fn, n) {
function run() {
fn.timeout = null;
var timer = n || new Date;
fn();
if (!n) duration = (new Date) - timer;
}
var duration;
return function postpone() {
if (fn.timeout)
clearTimeout(fn.timeout);
fn.timeout = setTimeout(run, n || duration || 100);
};
}


You might of course want to add some argument and this re-binding to it, for cases where the callback isn't self-contained, but then it gets a bit more cluttered, and I really just wanted to show the principle here. Typical usage is replacing your node.addEventListener("mousemove", callback, false) with the wrapped node.addEventListener("mousemove", expensive(callback), false) version, and suddenly, the UI is not only bearable, but probably even rather pleasant to use.