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!