Home > dojo, Event Handling, Javascript > Debouncing Javascript Methods

Debouncing Javascript Methods

March 20th, 2009

Back in 2006, I was the lead front-end architect for a mission-critical Web 2.0 application. Yah-yah, aren’t they all mission-critical? Yes, but this one really was critical since it was one of those make-or-break moments for the product. What made this project really interesting were the unrealistic expectations the product managers had of web apps.

Sometimes, pressure like this can lead to creativity and innovation. On this occasion, it helped me reach back in to my electrical engineering coursework to apply a hardware concept to software programming.

Just imagine this:

  1. You are tasked to write the entire Javascript-based framework from scratch, including the core functions (inheritance, declarations, event connections), DOM manipulation and querying, a widget framework, MVC, drag-and-drop, and dozens of custom widgets to boot.
  2. You are given unintuitive and un-web-like requirements to ensure that the app allowed customers to adhere to strict FDA regulations.
  3. The product managers had no experience with web app development so they designed the requirements for a Windows application. You know: modal dialogs, instant retrieval of large amounts of data (as if it were coming straight from the disk), workflows that depended on sequenced calls to/from the back-end systems, etc.

Oh, and did I mention there was a deadline and only two of us were hired to code the entire UI side of the project?

Well, I am always up for a challenge, especially if somebody says, “It can’t be done”. So needless to say, I had to really use my brain on this project.

One of the more interesting requirements was to show the user a summary side-bar of information pertaining to the current field (i.e. the input element that had focus). This side-bar information could be from several kilobytes to more than 100 kB. The data had to be fetched via XHR from the back-end and displayed each time the user entered a new field.

This presented a major problem if the user was keyboard-oriented and preferred to use the Tab key to move from field to field. There could be hundreds of fields on the screen, and a user could simply lean on the Tab key to navigate from one end of the form to the other. If the user were lucky enough to be on a fast network connection and had a fast enough browser, this would go fairly smoothly. However, the server would get absolutely hammered by all of the XHR requests necessary to populate the side-bar!

Luckily, I had handled this situation several times before. A simple setTimeout and a few state variables fixes this problem fairly easily. But there were several of these situations in this project, and I wanted to build something reusable.

The more I thought about it, the more it felt like contact 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.)

Debouncing != Throttling

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.

Implementation

The original debounce method was rather large and clunky. I consider it a prototype. A proof of concept. I ended up needing debouncing a few times since that project and rewrote it each time, trying to improve it. I’ve finally devised something that feels good enough to share.

The latest rendition takes two parameters: the detection period (“threshold”) and a Boolean indicating whether the signal should happen at the beginning of the detection period (true) or the end (“execAsap”).

Here it is:

Function.prototype.debounce = function (threshold, execAsap) {
    var func = this, // reference to original function
        timeout; // handle to setTimeout async task (detection period)
    // return the new debounced function which executes the original function only once
    // until the detection period expires
    return function debounced () {
        var obj = this, // reference to original context object
            args = arguments; // arguments at execution time
        // this is the detection function. it will be executed if/when the threshold expires
        function delayed () {
            // if we're executing at the end of the detection period
            if (!execAsap)
                func.apply(obj, args); // execute now
            // clear timeout handle
            timeout = null; 
        };
        // stop any current detection period
        if (timeout)
            clearTimeout(timeout);
        // otherwise, if we're not already waiting and we're executing at the beginning of the detection period
        else if (execAsap)
            func.apply(obj, args); // execute now
        // reset the detection period
        timeout = setTimeout(delayed, threshold || 100); 
    };
}

It works by issuing a setTimeout at the specified detection period. Each time the function is called, the setTimeout is canceled and issued again. This serves as our detection mechanism, but using reverse logic. If/when the setTimeout executes, we know that our function was not called within the detection period.

Wow. It looks rather large with all of those comments. Here is a version without the comments:

Function.prototype.debounce = function (threshold, execAsap) {
 
    var func = this, timeout;
 
    return function debounced () {
        var obj = this, args = arguments;
        function delayed () {
            if (!execAsap)
                func.apply(obj, args);
            timeout = null; 
        };
 
        if (timeout)
            clearTimeout(timeout);
        else if (execAsap)
            func.apply(obj, args);
 
        timeout = setTimeout(delayed, threshold || 100); 
    };
 
}

If you prefer not to augment the native objects, here’s a stand-alone version:

var debounce = function (func, threshold, execAsap) {
 
    var timeout;
 
    return function debounced () {
        var obj = this, args = arguments;
        function delayed () {
            if (!execAsap)
                func.apply(obj, args);
            timeout = null; 
        };
 
        if (timeout)
            clearTimeout(timeout);
        else if (execAsap)
            func.apply(obj, args);
 
        timeout = setTimeout(delayed, threshold || 100); 
    };
 
}

Example uses:

// using debounce in a constructor or initialization function to debounce 
// focus events for a widget (onFocus is the original handler):
    this.debouncedOnFocus = this.onFocus.debounce(500, false);
    this.inputNode.addEventListener('focus', this.debouncedOnFocus, false);
 
 
// to coordinate the debounce of a method for all objects of a certain class, do this:
MyClass.prototype.someMethod = function () {
    /* do something here, but only once */
}.debounce(100, true); // execute at start and use a 100 msec detection period
 
 
// the dojo way to coordinate the debounce of a method for all objects of a certain class
// (using the stand-alone version)
dojo.declare('MyClass', null, {
 
    // other members go here
 
    someMethod: debounce(function () {
        /* do something here, but only once */
    }, 100, true); // execute at start and use a 100 msec detection period
 
});
 
 
// wait until the user is done moving the mouse, then execute
// (using the stand-alone version)
document.onmousemove = debounce(function (e) {
    /* do something here, but only once after mouse cursor stops */
}, 250, false);

Let me know if you find this useful in your projects!

P.S. The mission-critical project was a success!

dojo, Event Handling, Javascript

  1. March 23rd, 2009 at 13:33 | #1

    Impressive article. Reminds a bit of how the hoverIntent plugin for jQuery works.

  2. March 23rd, 2009 at 15:12 | #2

    Nice article. I was just about to address this same issue today in some code I was working on, so this nice re-usable wrapper is highly timely for me!

  3. March 23rd, 2009 at 21:56 | #3

    Very cool! I’ve run into this problem many times and never gotten around to solving it.

  4. March 23rd, 2009 at 22:51 | #4

    Your “debounce” is a reinvention of the “calm” operator from functional-reactive programming (FRP). Flapjax is an existing FRP library (and language, if you like) for JavaScript that supports this, along with many other useful functional-reactive operators. (Disclosure: I helped develop this library.) For example, the mergeE operator takes two callbacks and combines them: mergeE($E(‘submit_top’,’click’),$E(‘submit_bottom’,’click’)) will “call back” when an onclick event occurs on either submit_top or submit_bottom. What’s great about Flapjax is that it allows you manipulate callbacks — and the dataflowing from them — directly.

    The delicious example is a good place to start; there are four versions of increasing complexity. There’s a tutorial, too, though it focuses on Flapjax the language, rather than the library.

  5. Straps
    March 24th, 2009 at 03:53 | #5

    Radical and very useful approach, thanks

  6. March 24th, 2009 at 05:12 | #6

    Impressive piece of code. I’ll definitely find this useful some day. I bookmarked it.

  7. March 24th, 2009 at 09:55 | #7

    > 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.

    Most keyboards have a bounce time of ~8ms. I found out at my expanse when I bought a 50$ Microsoft keyboard which have a bounce time of 15ms, most people will never notice, but I did.

    Don’t buy this s**t: http://www.microsoft.com/hardware/mouseandkeyboard/ProductDetails.aspx?pid=083&active_tab=systemRequirements

    With this keyboard when I type “Shift + :w + Enter” in Vim to save a document, the computer actually gets “:W + Enter”, which triggers an error in Vim. It’s pretty annoying, I guess Microsoft’s keyboard designer a slow typists.

    At first I thought my fingers were becoming lazy, but I realized it was the s***ty Microsoft keyboard when I bought a Razer Lycosa, one of the best keyboard I ever had for programming.

    With its 1ms response time this problem *never* occurred once. It also looks pretty bad ass: http://www.razerzone.com/gaming-keyboards/razer-lycosa/razer-lycosa-keyboard

    • March 24th, 2009 at 13:59 | #8

      Hey h3,

      Your post is a bit off-topic, but I share your sentiment about Microsoft products so I’ll let it through! :-P

      There are some good lessons to learn from your experience that can also be applied to Javascript debouncing:

       • Setting the detection period too high could cause missed events.
       • Setting the detection period too low could cause redundant events.
       • Coordinating multiple debounce routines can be tricky. (In your case, the Shift and other keys probably had different debounce circuitry.)
       • The quality of the underlying system is critical to being able to lower the detection period to a level that is neither missing events nor causing redundant events.

      That last point is key. If you’re debouncing native browser events, you don’t really have much choice. However, if you’re debouncing your own methods/events, then fast-performing Javascript can make a huge difference.

      Thanks for the post!

  8. Naeem Bhatti
    March 24th, 2009 at 12:30 | #9

    Good stuff.

  9. Alejandro
    March 26th, 2009 at 19:42 | #10

    Just wanted to let you know that this article has made my life much better. Thanks. Great job

  10. March 30th, 2009 at 15:18 | #11

    interesting article. i had to write the same thing once, so it was interesting to see how you did it. My function was a jquery plugin and probably not as nice. You may be interested to check it out:
    http://notetodogself.blogspot.com/2008/12/jquery-responsiveness-plugin-for-fast.html

  11. June 8th, 2009 at 15:12 | #12

    Brilliant, thanks! :)

  12. armdros
    August 21st, 2009 at 01:23 | #13

    Hi, I wanna use this script in IE, but I can`t get the event keyCode after debounce. how can i get it?

    test code here:

    var ttt = function(e){
    	if(!e && parent.window.event) { e = parent.window.event;}
            alert(e.keyCode); // error !!
     
    }.debounce(200,false);
    this.inputbox.attachEvent("onkeyup", ttt, false);
  13. August 21st, 2009 at 12:35 | #14

    Hey armdros, I think you’ve discovered something … um … interesting.

    It appears that IE clobbers* the event object once the original function call goes out of scope. It should not do this since we are holding a reference to the event object! This is obviously a bug in IE.

    There is a work-around: just debounce the portion of the handler that needs to be debounced and process the event in a non-debounced function.

    Example:

    // the debounced part - notice we don't pass the event object
    var ttt = function (code) {
        alert(code);
    }.debounce(200);
     
    // the attached event handler extracts the keycode ad passes it to the debounced function
    this.inputbox.attachEvent('onkeypress', function (e) { ttt((e || window.event).keyCode) });

    I hope that helps!

    – John

    * It’s not really clobbered, but I am not sure how else to explain it. The object exists, but it no longer has any members and raises exceptions if accessed: “Member not found.”

  14. armdros
    August 28th, 2009 at 03:30 | #15

    Hey John, I appreciate you for your help!!
    I Think You are very clever. Thankyou

  15. October 9th, 2009 at 15:07 | #16

    Very helpful, thanks!

  16. April 20th, 2010 at 18:34 | #17

    @h3
    That’s not really bounce, but more like threshold. I you press a key less then 15ms and it doesnt register the keypress then it’s a threshold problem, or whatever they call it. Bounce refers to multiple contiguous events reduced to a same event. Your problem is one (or more) event(s) reduced to zero events :)
    IMHO!

  1. March 23rd, 2009 at 08:08 | #1
  2. March 23rd, 2009 at 10:28 | #2
  3. March 23rd, 2009 at 22:27 | #3
  4. March 23rd, 2009 at 23:01 | #4
  5. April 23rd, 2009 at 10:49 | #5
  6. May 5th, 2009 at 15:47 | #6
  7. August 11th, 2009 at 20:06 | #7
  8. August 31st, 2009 at 05:11 | #8
  9. November 29th, 2009 at 09:20 | #9
  10. December 2nd, 2009 at 06:26 | #10
  11. June 29th, 2011 at 13:17 | #11
  12. July 27th, 2011 at 06:58 | #12
  13. August 21st, 2011 at 11:05 | #13
  14. April 17th, 2012 at 18:01 | #14
  15. February 26th, 2013 at 05:45 | #15
Comments are closed.