JavaScript function throttling and debouncing

Ben Alman has written up a useful little jQuery plugin for addressing situations where a JavaScript function in your web page is getting called a tad too frequently and you want to reduce the frequency to more manageable levels. He talks about the plugin over here. I figured it'd be fun to produce my own (possibly naive) implementations of the idea just for kicks and generally talk about it here (you might want to use Ben Alman's implementation for production code though - this article simply talks about one possible implementation of the idea). First, what exactly do we mean by throttling and debouncing?

Throttling

Imagine a scenario where you have an event handler written for some event that tends to get raised a bit too often. Handlers for the mouse move and the window scroll events are good examples. Imagine that you need to do something heavyweight from these handlers - like making an AJAX call. In this case, you probably do not really need to make that big expensive AJAX call every single time the event is fired. Things would probably work just as fine if the function was called, say, no more than 3 times a second. Here's where function throttling helps. The idea is to basically build a thin wrapper function that can moderate calls to your actual routine by providing an intercepting routine that rations invocations based on call frequency. Here's one way how you might implement the throttling routine:

//
// Throttle calls to "callback" routine and ensure that it
// is not invoked any more often than "delay" milliseconds.
//
function throttle(delay, callback) {
    var previousCall = new Date().getTime();
    return function() {
        var time = new Date().getTime();

        //
        // if "delay" milliseconds have expired since
        // the previous call then propagate this call to
        // "callback"
        //
        if ((time - previousCall) >= delay) {
            previousCall = time;
            callback.apply(null, arguments);
        }
    };
}

And here's how you might choose to use the function from a web page to register a handler for the window's scroll event and make sure that it is not invoked more than once every 300 milliseconds.

window.onscroll = throttle(300, function() {
    //
    // Big expensive AJAX call here
    //
});

Or if you are a jQuery kind of person then you might do it this way:

$(window).scroll(throttle(300, function() {
    //
    // Big expensive AJAX call here
    //
}));

Now one thing that you might have noted about this implementation is that it is a bit lossy - in that the data that is passed to the event handler tends to go wherever it is that ignored data like to go whenever the wrapper routine decides to not propagate the call to the actual routine. In this case the scroll position information is lost. In most cases, this is of no significance and you're probably quite happy knowing the current scroll position when the throttled call does take place. But if you happen to have some weird requirement where no piece of data can be lost and would like the data from the intermediary calls to be accumulated and passed to the callback, well, then, here's another version of throttle that does exactly that. This implementation simply accumulates all the data and makes it available to the target routine via a property called "data" in the "this" object of the final callback routine.

//
// Throttle calls to "callback" routine and ensure that it
// is not invoked any more often than "delay" milliseconds.
// Accumulates all data passed to the callback routine during
// calls that are not passed on to "callback" and then hands
// it off to "callback" via a property called "data" on the
// context object for that call.  "data" is a (possibly jagged)
// 2 dimensional array containing the data accumulated during
// all the calls to the throttled function since the previous
// expiration of "delay" milliseconds.
//
function throttle(delay, callback, accumulateData) {
    var previousCall = null;
    var theData = [];
    return function () {
        var time = new Date().getTime();

        //
        // accumulate arguments in case caller is interested
        // in that data
        //
        if (accumulateData) {
            var arr = [];
            for (var i = 0; i < arguments.length; ++i)
                arr.push(arguments[i]);
            theData.push(arr);
        }
        if (!previousCall ||
            (time - previousCall) >= delay) {
            previousCall = time;
            callback.apply((accumulateData) ? { data: theData} : null, arguments);
            theData = []; // clear the data array
        }
    };
}

In case you happen to be that one other person on the internet who I hear has an interest in Common Lisp and JavaScript at the same time, then maybe you'd appreciate a Common Lisp version of the throttling routine (the version that ignores data passed to the routine during intermediary calls that are not propagated to the actual callback).

;;
;; throttles invocations to routine identified by "callback"
;; so that it is invoked no more frequently than "delay" interval;
;; "delay" is interpreted as being a measure of "internal-time-units-per-second"
;; units;
;;
(defun throttle (delay callback)
    (let ((previousCall nil))
        #'(lambda (&rest p)
            (if (or (not previousCall)
                    (>= (- (get-internal-real-time) previousCall) delay))
                (progn
                    (setf previousCall (get-internal-real-time))
                    (apply callback p))))))

Debouncing

Debouncing is also a technique for managing the frequency of calls that a routine receives. The difference between debouncing and call throttling might seem a bit subtle at first, but it really isn't. While throttling is about simply restricting the frequency of calls that a function receives to a fixed time interval, i.e. ensuring that the target function is not invoked more often than the specified delay, debouncing is about coalescing multiple calls that happen on a given routine so that repeated calls that occur before the expiration of a specific time duration are ignored. OK, that sounds confusing to me and I just wrote it. Let's take a sample scenario.

Imagine that you're writing some kind of online collaboration app and you want to provide a multi-user whiteboard feature of some sort that'll let multiple participants doodle on the whiteboard simultaneously for some reason. Now, a couple of approaches immediately spring to mind:

  • Capture mouse co-ordinates as the lines are being drawn and broadcast them to all the participants. This approach provides instant feedback of the doodling process as it takes place but results in an exponential number of network packets being transmitted between participants and could also result in overloading the server with requests. Clearly, this is not going to scale.
  • Wait for complete line segments to be drawn before transmitting the path co-ordinates to everybody. Admittedly, this approach is a pretty decent one and something that users are quite likely to find as being an acceptable compromise. Having said that however, I think we can do better! The doodling process usually involves little breaks as lines are being drawn - I might draw a path and then pause a bit before deciding where to head off next. Wouldn't it be great if we could leverage those "pauses" to quickly dispatch the co-ordinates to everybody? That's exactly what debouncing allows you to do.

Debouncing is the process of translating multiple calls to a function that occur during a given time period to a single call (so far this is exactly the same as call throttling) with the added caveat that the receipt of a new call to the function before the expiration of the specified delay results in a resetting of the time to wait before a call to the actual routine can be propagated next. For instance, if the established delay is 250ms, then every call to the debounced routine before the expiration of 250ms results in a fresh wait being initiated for another 250ms. Once 250ms expire, a single call is made to the actual routine. Here's a sample implementation:

//
// Debounce calls to "callback" routine so that multiple calls
// made to the event handler before the expiration of "delay" are
// coalesced into a single call to "callback". Also causes the
// wait period to be reset upon receipt of a call to the
// debounced routine.
//
function debounce(delay, callback) {
    var timeout = null;
    return function () {
        //
        // if a timeout has been registered before then
        // cancel it so that we can setup a fresh timeout
        //
        if (timeout) {
            clearTimeout(timeout);
        }
        var args = arguments;
        timeout = setTimeout(function () {
            callback.apply(null, args);
            timeout = null;
        }, delay);
    };
}

You might use this routine like so to debounce the mouse move event handler:

window.mousemove = debounce(250, function() {
    //
    // Big expensive AJAX call here
    //
});

As before, this should work just as well if you're into jQuery:

$(window).mousemove(debounce(250, function(event) {
    //
    // Big expensive AJAX call here
    //
}));

Just as with throttling, the implementation given above results in the data that was passed to the event handler during calls that end up getting ignored being lost. In case you need access to all the data when the call is finally propagated to the target routine, then you might want to go with the following version of debounce:

//
// Debounce calls to "callback" routine so that multiple calls
// made to the event handler before the expiration of "delay" are
// coalesced into a single call to "callback". Also causes the
// wait period to be reset upon receipt of a call to the
// debounced routine. Accumulates all data passed to the callback
// routine during calls that are not passed on to "callback" and
// then hands it off to "callback" via a property called "data"
// on the context object for that call.  "data" is a (possibly
// jagged) 2 dimensional array containing the data accumulated
// during all the calls to the debounced function since the
// previous expiration of "delay" milliseconds.
//
function debounce(delay, callback, accumulateData) {
    var timeout = null;
    var theData = [];
    return function () {
        //
        // accumulate arguments in case caller is interested
        // in that data
        //
        if (accumulateData) {
            var arr = [];
            for (var i = 0; i < arguments.length; ++i)
                arr.push(arguments[i]);
            theData.push(arr);
        }

        //
        // if a timeout has been registered before then
        // cancel it so that we can setup a fresh timeout
        //
        if (timeout) {
            clearTimeout(timeout);
        }
        var args = arguments;
        timeout = setTimeout(function () {
            callback.apply((accumulateData) ? { data: theData } : null, args);
            theData = []; // clear the data array
            timeout = null;
        }, delay);
    };
}

And here's an example showing how you might use this version of debounce for handling the mouse move event (assume that print is a function that logs data to a console of some kind):

$(window).mousemove(debounce(250, function (event) {
    print("Mouse moved: ");
    for (var i = 0; i < this.data.length; ++i) {
        print("(" + this.data[i][0].pageX + ", " + this.data[i][0].pageY + ")");
    }
}, true));

What makes the implementation somewhat straightforward is the judicious use of JavaScript closures. In both the implementations, closures allow us to track local state on a per function basis across multiple calls without having to resort to global variables and such. Pretty neat in my opinion!

comments powered by Disqus