Monday, March 23rd, 2009

Debounce your JavaScript functions

Category: Articles, JavaScript, Library

<>p>John Hann has written an enjoyable post on debouncing JavaScript methods that comes with a fun back story on a project that John worked on.

John gets to the matter of debouncing:

Debouncing means to coalesce several temporally close signals into one signal. For example, your computer keyboard does this. Every time you hit a key, the contacts actually bounce a few times, causing several signals to be sent to the circuitry. The circuitry determines that the bouncing has ended when no bounces are detected within a certain period (the “detection period”). Since people can’t really type faster than roughly 10 keys per second, any signals happening within 100 msec of each other, for example, are likely part of the same key press. (In practice, you should at least halve this, so about 50 msec for our keyboard example. I have no idea what keyboards really use, by the way. This is just an illustration.)

Whenever I bring up the concept of debouncing, developers try to cast it as just a means of throttling. But that’s not true at all. Throttling is the reduction in rate of a repeating event. Throttling is good for reducing mousemove events to a lesser, manageable rate, for instance.

Debouncing is quite more precise. Debouncing ensures that exactly one signal is sent for an event that may be happening several times — or even several hundreds of times over an extended period. As long as the events are occurring fast enough to happen at least once in every detection period, the signal will not be sent!

Let’s relate this back to our keyboard-oriented user and our huge set of form fields. Throttling here would certainly help. We could reduce the number of XHR requests to a lower rate than the computer’s key repeat rate for sure! However, we’d still be fetching from the back-end more times than necessary, and the occasional re-rendering of the fetched data could temporarily freeze up the browser, deteriorating the user experience.

Debouncing on the other hand could better detect when the user stopped leaning on the keyboard and had arrived at their destination. It’s certainly not perfect. The user still may overshoot their destination, hesitate, and back-track, causing enough delay for the debounce detection period to expire. However, our tests showed that debouncing did a much better job of reducing XHR requests than throttling.

John then shows this in code:

javascript
< view plain text >
  1. Function.prototype.debounce = function (threshold, execAsap) {
  2.     var func = this, // reference to original function
  3.         timeout; // handle to setTimeout async task (detection period)
  4.     // return the new debounced function which executes the original function only once
  5.     // until the detection period expires
  6.     return function debounced () {
  7.         var obj = this, // reference to original context object
  8.             args = arguments; // arguments at execution time
  9.         // this is the detection function. it will be executed if/when the threshold expires
  10.         function delayed () {
  11.             // if we're executing at the end of the detection period
  12.             if (!execAsap)
  13.                 func.apply(obj, args); // execute now
  14.             // clear timeout handle
  15.             timeout = null;
  16.         };
  17.         // stop any current detection period
  18.         if (timeout)
  19.             clearTimeout(timeout);
  20.         // otherwise, if we're not already waiting and we're executing at the beginning of the waiting period
  21.         else if (execAsap)
  22.             func.apply(obj, args); // execute now
  23.         // reset the waiting period
  24.         timeout = setTimeout(delayed, threshold || 100);
  25.     };
  26. }

There is also a version that doesn’t augment Function if that is more to your fancy.

Finally, some uses:

javascript
< view plain text >
  1. // using debounce in a constructor or initialization function to debounce
  2. // focus events for a widget (onFocus is the original handler):
  3. this.debouncedOnFocus = this.onFocus.debounce(500, false);
  4. this.inputNode.addEventListener('focus', this.debouncedOnFocus, false);
  5.  
  6. // to coordinate the debounce of a method for all objects of a certain class, do this:
  7. MyClass.prototype.someMethod = function () {
  8.     /* do something here, but only once */
  9. }.debounce(100, true); // execute at start and use a 100 msec detection period
  10.  
  11. // wait until the user is done moving the mouse, then execute
  12. // (using the stand-alone version)
  13. document.onmousemove = debounce(function (e) {
  14.     /* do something here, but only once after mouse cursor stops */
  15. }, 250, false);

Related Content:

15 Comments »

Comments feed TrackBack URI

Good writeup — been doing this for a while, always try to use a delay of about 500ms, I think. Usually works out best.

Comment by mdmadph — March 23, 2009

Cool stuff.

We’ve actually written a dojo component that does a similar thing. We’ve developed a TextBox that we use for simple searches: 1 search field that performs an xhr request as you type (no “Go” button click required, we respond on keyup events). Much like Apple’s spotlight search box for example, but in a web context.

The dojo component is configurable in 2 parameters: the delay, and the amount of characters that must be minimally typed before a request is sent. Some keypresses are filtered out (such as arrow keys, function keys).

You can find the code here

Comment by TomMahieu — March 23, 2009

Thanks @mdmadph and @TomMahieu. You guys nailed it on the head: we’ve all been doing this in one way or another. I guess I just needed to put a new name on it (since it’s certainly not throttling) and create one function that can be extended to many cases.

I found I was writing this sort of functionality at least once in every project. Before I made it a core function, though, it was much harder for a subsequent programmer to figure out what was happening. Calling myFunc.debounce() is much much clearer, imho, than setTimeout().

@mdmadph: Agreed! For user-oriented actions, somewhere around 500 msec is a good starting point.

@TomMahieu: this debounce function could be used as is in your case if the debounced method checked for minimal characters. It also wouldn’t be too hard to extend the debounce function to take an additional Boolean function to check additional rules before firing the debounced method.

Comment by unscriptable — March 23, 2009

I would wonder about the case of a function which takes longer to execute than the specified timeout.

Comment by nataxia — March 23, 2009

In ExtJS, one can use the ‘buffer’ property to handle events within that timespan as one single event. It ensures that events can’t be thrown too fast, and it is easy as it could possibly get to implement.

Comment by PieturP — March 23, 2009

I wrote a similar function a year ago, and I still use it often – very useful. Glad this trick got attention on Ajaxian, well done John!

Function.prototype.limitExecByInterval = function(time) {
var lock, execOnUnlock, args, fn = this;
return function() {
args = arguments;
if (!lock) {
lock = true;
var scope = this;
setTimeout(function(){
lock = false;
if (execOnUnlock) {
args.callee.apply(scope, args);
execOnUnlock = false;
}
}, time);
return fn.apply(this, args);
} else execOnUnlock = true;
}
}

Comment by Mourner — March 23, 2009

I like it. I also like the design in that it extends the function prototype to have it easily accessible. (spot the Prototype fan)

Thanks for sharing!

Comment by RoryH — March 23, 2009

Aforementioned code is so ugly. I suggest this:

Function.prototype.moderate = function(delay, bind) {
var time, fn = this;
return function() {
if (!time || (new Date().getTime() - time) >= delay) {
time = new Date().getTime();
return fn.apply(bind || null, arguments);
}
};
};

Comment by steida — March 23, 2009

… also all solutions based on setTimeout are brittle.

PS: in comments doesn't work.

Comment by steida — March 23, 2009

… also all solutions based on setTimeout are brittle.
PS: code tag in comments doesn’t work

Comment by steida — March 23, 2009

@nataxia: there is no danger from a long-running function as with some throttling implementations.

@PieturP: thanks for the heads-up on the ExtJS implementation. I’ll check it out!

@Mourner ad @RoryH: thanks for the kudos!

@steida: I don’t know why you think that setTimeout is so brittle. Care to elaborate? Btw, your implementation only works if the triggering event is guaranteed to occur at least once after the delay expires. The solutions presented by Mourner and I are “guaranteed” to execute once — unless, of course, you think that setTimeout is too “brittle” to guarantee anything.

Comment by unscriptable — March 23, 2009

@steida: I believe what you’ve posted is yet another throttling implementation. I suggest you read the whole blog post to understand why debouncing != throttling.

Comment by unscriptable — March 23, 2009

Steida,
why is setTimeout brittle?

Comment by jaysmith — March 25, 2009

Unscriptable – thank you for explanation.
Jaysmith – setTimeout is brittle where time accuracy is needed. Also, setTimeout breaks code flow. Nothing crucial, but sometime important.

Comment by steida — March 28, 2009

Wrote up a couple of simple debounce and throttle implementations on jQuery:

jQuery.debounce = function(callback, delay) {
var timeout;
return function() {
clearTimeout(timeout);
timeout = setTimeout(callback, delay);
}
}

jQuery.throttle = function(callback, delay) {
var prev = new Date().getTime() – delay;
return function() {
var now = new Date().getTime();
if (now – prev > delay) {
prev = now;
callback.call();
}
}
}

Comment by badabim — March 29, 2009

Leave a comment

You must be logged in to post a comment.