Debouncing, Throttling and Queueing

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

Debouncing, Throttling and Queueing article, written by Kartones. Published on