Note: debouncing
Adding debounce to a function results in the function is only run once a break has occurred in triggering.
Simple debounce #
If a user is typing in an input field (a search field, for example) you don’t want to perform a HTTP request for each keystroke. Debouncing the keystroke listener will result in a HTTP request being sent once the user stops (or has a break in) typing and not before.
Debouncing is implemented with a debounce function, which takes a inner function (the action) and a delay (the time that should pass with no triggering before the action is taken).
My best suggestion for a simple debounce function (after writing something, then looking at the one in lodash, the one in underscore, and finally stumbling upon a great one by @rem) looks something like this:
function debounce(fn, delay) {
if(typeof delay == "undefined") {
delay = 250;
}
var timer = null;
return function () {
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
}
This function has a default delay of 250 ms, which I found reasonable for a keyboard-native typing into an input field. A longer delay would make sense on mobile devices as longer breaks between keystrokes are likely.
Without debouncing you might have set up an input listener like this:
var searchInput = document.querySelector("#myForm input.searchField");
searchInput.addEventListener("keyup", function(e) {
search();
});
All you need to change to add debouncing is wrapping the search
function in your debounce function before setting up the input listener:
search = debounce(search);
This allows you to, more or less indiscriminately, trigger searches from anywhere on the page: only if no other search has been triggered within 250 ms will it actually run.
Batching and debouncing #
Here’s another case where you would want to wait for a break in actions before sending a request to the server: synchronizing state.
If your frontend application lets the user take several types of actions (eg. “move an element”, “rename an element”, “assign a user to an element”) it might be inefficient to send off an HTTP request as soon as the user takes an action: when the user takes several actions in quick succession a lot of HTTP requests would be triggered.
For example, consider this interface which would synchronize a user action:
synchronizeAction({
action: "move",
subject: element,
destination: list
});
Batching cannot immediately be achieved by a regular debounce function. The simple debounce function I showed above would only run the inner function with the last set of arguments it was called with (ignoring all previous calls).
For this case we need a batchDebounce
function, which would look something like this:
function batchDebounce(fn, delay) {
if(typeof delay == "undefined") {
delay = 1500;
}
var currentBatch = [];
return function(item) {
currentBatch.push(item);
var context = this;
var args = Array.prototype.slice.call(arguments);
args[0] = item;
clearTimeout(timer);
timer = setTimeout(function () {
currentBatch = [];
fn.apply(context, args);
}, delay);
};
}
We cannot simply pass synchronizeAction
to the batchDebounce
function because synchronizeAction
expects a single action and we are going to be passing an array of actions.
Batch-debouncing means that you need support for synchronizing several actions at once.
Assuming we have a function synchronizeAllActions
which supports synchronizing an array of actions, we can derive a batch-debounced synchronizeAction
function:
var synchronizeAction = batchDebounce(synchronizeAllActions);
I have set the default delay to 1.5s because these actions are typically performed with more time inbetween than keystrokes, but also because immediate synchronization is not required.
Is there another cool type of debounce (or debounce-like) functions? Let me know! I would love to add it to this note.
When I write more, filling out this form will let you know.