Throttling can be used in a myriad of scenarios, from reliability (avoid flooding with actions) to performance (not doing anything more times than needed in a specific time-frame). I was checking for ways to throttle some event handlers in Javascript for a personal experiment, and gathered 3 (well, in practice 4) approaches, each with different subtleties. So, to keep them at hand I've decided to write this small post.
Debounce
Debouncing a function means delaying executing the function/callback until after a certain delay. The function will only execute once, no matter how many times you trigger it.
Example code (source):
function debounce(callback, delay) {
let timeout;
return function() {
const context = this;
let later = function() {
timeout = null;
callback.apply(context);
};
clearTimeout(timeout);
timeout = setTimeout(later, delay);
}
}
Leading Edge Debounce
Pretty similar to the normal debounce, but instead of waiting before executing the callback, in this mode (also known as immediate debounce) we first execute the callback and then disallow further invocations until the delay is over.
Example code (source):
function debounce_plus(func, wait, immediate) {
let timeout;
return function() {
const context = this, args = arguments;
let later = function() {
timeout = null;
if (!immediate)
func.apply(context, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow)
func.apply(context, args);
};
};
Throttle
Throttling is an old but always effective technique. In its simplest form, we only allow at most one function execution in the specified threshold (very similar to immediate debouncing). Further calls are discarded.
Example code (source):
function throttle(callback, threshold) {
let last;
let timeout;
return function() {
const context = this;
const now = Date.now();
if (last && now < last + threshold) {
window.clearTimeout(timeout);
timeout = window.setTimeout(function() {
last = now;
callback.apply(context);
}, threshold);
} else {
last = now;
callback.apply(context);
}
};
}
Queue
Queueing is another mature technique, by which we build a "tasks list" and then execute it (usually in a certain order).
The benefits of this technique are mainly that a) you don't lose any invocation request (you enqueue it 3 times, 3 times will be executed) and b) you get fine control of when it will be executed, and can do any kind of operations over it (throttle the dequeueing speed, remove duplicates/aggregate similar calls, etc.).
Example code (source), buffering into a FIFO queue and adding an initial processing start delay:
function Buffer(startDelay) {
var queue = [];
let timeout;
function taskHandler(task, callback) {
console.log("Queue size: " + queue.length);
task();
// call the buffer callback.
callback();
}
function run() {
var callback = function () {
if (queue.length > 0) {
run();
} else {
// signal the buffer we're done processing
timeout = null;
}
}
// give the first item in the queue & the callback to the handler
taskHandler(queue.shift(), callback);
}
// push the task to the queue. If the queue was empty before the task was pushed we start processing
this.append = function(task) {
queue.push(task);
// Simulate a delayed start. In the real world, should just be what's inside of the function,
// and there would be no need of a timeout except if we want a delayed start of queue processing
if (queue.length && !timeout) {
clearTimeout(timeout);
timeout = window.setTimeout(function() {
run();
}, startDelay);
}
}
}
A minimalistic working example can be checked at https://kartones.net/demos/023/. Just some inputs, each applying a different technique to the onkeyup
event.
Tags: Development