Managing JavaScript events/functions using "debouncing"

Posted by Dan on Mar 24, 2009 @ 12:20 PM

Yesterday I came across a very timely post by John Hann on Debouncing Javascript Methods. I say timing, because this was a problem I was just getting ready to solve again for umpteenth time—managing rapidly fired events in JavaScript.

I was working on some live search functionality for a page where I was searching DOM elements and populating a list of matching elements. Since this was a "live" search, I'm doing the update as the user types. The trick to this issue is that you don't really want to fire off the process each time the user presses a key, but instead you want to really fire it off when there's been a delay/pause to their typing. If you actually do the processing on each character, many of the calls you're making become quickly invalidating as the input changes so quickly that you end up making unnecessary calls. You'll also see a noticeable slow down in performance if your process is CPU intensive.

You can run into the same issue when dealing with AJAX operations. If you're just updating some portion of the screen based upon some user interaction, you really only want to fire off the AJAX call when the user is done interacting with the element.

This is where John's debounce solution comes into play.

In the past, I've always rolled up some solution manually and never came up with a particularly good re-usable solution (which John's has done with the debounce functions.) The idea John's using is essentially the same one I've used—fire off an async event using setTimeout() with a short delay which is cancelled if there's another request to the same function within the delay. What this does is make sure we only run the function when it really matters. This means if we fire off a function 10 times in 1 second, we only ever end up triggering the logic the last time we call the function (because the first 9 iterations become outdated so quickly, there's no point in run the process.)

What John has done is come up with a re-usable function that handle all the heavy lifting for you. No longer do you need to worry about handling this logic within your function, the debounce() function handles it for you.

Let's look at John's code:

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);
    };
}

While the function itself isn't very big, the syntax may look confusing if you're not well versed in JavaScript. In a nutshell, what happens is your original function is replaced by a copy of the function that is debounced. So, if the function is called multiples times within the threshold value, then only the calls that occur outside of that threshold are actually processed.

Since this concept is easier grasps seeing a live example, let's take a look at one.

In the following example, we will monitor each key press and output the value. Here's the source code we're using:

document.getElementById("ex1").onkeypress = function (){
  // run update
  keyTest("ex1");
};

Now, trying typing in the box below. You'll notice that every keystroke you press registers an update to the screen.

Type:

Now let's take a look at the same example utilizing the debounce technique. Here's the source code:

document.getElementById("ex2").onkeypress = function (){
  // run update
  keyTest("ex2");
}.debounce(500);

The only key difference is that we've added the debounce() function after the end of the declaration of the function. This works because debounce() was defined as a method of the Function object. The argument value of 250 is telling the debounce function to only run the function if 250ms has passed since the last time the function was called.

Now try typing in the box below. You'll notice that updates only appear when there's a pause in your typing. If you are typing more than 1 character a second, then the updates will not appear until you stop typing.

Type:

NOTE:
While I've used 500ms for the debounce threshold, in my testing 250ms appears to be a better value for keyboard related events.

As you can see, the behavior in the debounce() version offers behavior that much more conducive to what we need—especially if you're dealing with AJAX-based operation. If you're firing off the event after each keystroke, you're just performing operations that are invalidated almost immediately and will only slow down performance of your application.

The debouce technique can really be used for lots of things besides just monitoring keystrokes on an element—you could use it to monitor mouse movement or to even handle mouse clicks. It comes in handy any time you really only want to trigger an operation once activity has settled down.

So thanks go to John Hann for such a handy little function!

Categories: HTML/ColdFusion

9 Comments

  • This is awesome! I have not experimented much with updating the prototype of built in objects, but this just looks cool!
  • @Ben:

    John also has a low level function version that you can use if you prefer not to pollute the built-in objects.

    If you use that, the code would look like this:

    document.getElementById("ex2").onkeypress = debounce(function (){
     // run update
     keyTest("ex2");
    }, 250);

    I used the prototype for this example because I thought maybe it made the functionality easier to understand.
  • @Dan,

    It's all very cool. Thanks for sharing.
  • Dan, this rocks! I was also just thinking about how to roll my own debounce-like functionality (didn't know the technique actually had a name) for a project I'm working on, but this is so much better! Thanks for posting this entry!
  • Hey Dan,

    Nice write-up and example! I think I'll send people here when they want a working example -- especially since I am too lazy to write one! ;-)

    And thanks for the kudos!

    -- John
  • @John:

    No, thank you! It's a great little helper function for handling a common problem.
  • Can you explain where execAsap is useful?

    Looks to me like this could be further simplified by removing that feature. Something like this:

    <pre><code>Function.prototype.debounce = function (threshold) {
      var func = this,
        timeout;
      threshold = threshold || 100;
      return function debounced () {
        var obj = this,
          args = arguments;
        function delayed () {
          func.apply(obj, args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(delayed, threshold);
      };
    }</code></pre>
  • @Jörn:

    The execAsap parameter is probably more of a corner case usage.

    If we're looking solely at keyboard entry, then if you're interacting with a series of characters (such as the user typing in a text field) you're generally going to want to process when the typing stops (which is the default behavior.)

    However, maybe you're monitoring mouse clicks so you want to trigger something on the first click and essentially "ignore" the additional clicks--that's when running it ASAP would be handy.

    You could definitely simplify the code for specific use cases, but John Hahn tried to develop a re-usable piece he could use for many use cases.
  • Very sweet bit of code. Aaargh!

Comments for this entry have been disabled.