DEV Community

Cover image for Debouncing TypeScript
Matt Kenefick
Matt Kenefick

Posted on

Debouncing TypeScript

What is it?

"Debouncing" is a term that means "preventing a function from executing too frequently."

There are instances where a function may be executed way more often than necessary; usually unintentionally. Some instances of this are:

  • The Window scroll event
  • The Window resize event
  • RequestAnimationFrame
  • SetInterval @ 1000/60 (60 FPS)

By utilizing a debouncing technique, rather than executing your complicated code 60 times every second, you could execute it once or twice. You are likely to see noticeable performance improvements by integrating this optimization.

Example:

JSFiddle: https://jsfiddle.net/76gatsbj/6/

How does it work?

Debouncing works by comparing timestamps; usually Date.now(). It essentially says: "If the last time we ran this function was less than one second ago, then don't run it this time."

Logically, we do this by giving it a threshold of some kind (500ms, let's say) then making our time comparison. If the last execution exceeds our threshold + current timestamp, then we execute our function + save the current time.

Show me code

Here's a simple TypeScript debounce class.

/**
 * Debounce
 *
 * Prevents a function from being fired too often by determining
 * a difference in time from the last time in which it was fired
 *
 * @author Matt Kenefick <polymermallard.com>
 */
class Debounce 
{
    /**
     * Debounced function that we will execute
     *
     * @type function
     */
    public callback: () => void;

    /**
     * Time in between executions
     *
     * @type number
     */
    public threshold: number;

    /**
     * Last time this function was triggered
     *
     * @type number
     */
    private lastTrigger: number = 0;

    /**
     * @param function callback
     * @param number threshold
     * @return function
     */
    public constructor(callback: () => void, threshold: number = 200): () => void {
        this.callback = callback;
        this.threshold = threshold;

        return this.run.bind(this);
    }

    /**
     * Executable function that applies debounce logic
     * 
     * @return void
     */
    public run(): void {
        const now: number = Date.now();
        const diff: number = now - this.lastTrigger;

        if (diff > this.threshold) {
            this.lastTrigger = now;
            this.callback();
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

We can implement the debounce class above like so:

function myFunction() {
    console.log('This is the debounced function');
}

const event = new Debounce(myFunction, 500);

// Run via interval at 60FPS, execute function every 500ms
setInterval(event, 1000 / 60);
Enter fullscreen mode Exit fullscreen mode

Or we can apply it to the scroll event which fires frequently:

function myScrollFunction() {
    console.log('This fires on scroll every 200ms');
}

const event = new Debounce(myScrollFunction, 200);

// Run on document scroll, only execute every 200ms
window.addEventListener('scroll', event);
Enter fullscreen mode Exit fullscreen mode

Inclusive vs Exclusive

There are two ways we can approach debouncing techniques: inclusive or exclusive.

An inclusive approach will deny events happening too frequently, but create a single timeout that runs in the future even if the triggering event stops happening. This is usually beneficial for callbacks that have longer thresholds between executions and/or less frequent triggers.

For instance, let's say you have an event you want to fire every 3000ms on scroll. It's very possible the user may stop scrolling in between executions, but you may want the callback to fire one last time. This may be used to re-adjust a viewport that just appeared.

An exclusive approach only attempts to execute a callback when the triggers are being applied. In the example above with the 3000ms callback, we would only ever fire while the document is being scrolled, but never after it stops.

The code example earlier in the article represents an exclusive approach to debouncing.

Here's an example of inclusive debouncing: https://jsfiddle.net/719y2fwq/

Inclusive Debouncing

/**
 * InclusiveDebounce
 *
 * Prevents a function from being fired too often by determining
 * a difference in time from the last time in which it was fired.
 * 
 * Applies inclusive techniques to execute functions one last time.
 *
 * @author Matt Kenefick <polymermallard.com>
 */
class InclusiveDebounce
{
    /**
     * Debounced function
     *
     * @type function
     */
    public callback: () => void;

    /**
     * Time in between triggers
     *
     * @type number
     */
    public threshold: number;

    /**
     * Last time this function was triggered
     *
     * @type number
     */
    private lastTrigger: number = 0;

    /**
     * Timeout for calling future events
     *
     * @type number
     */
    private timeout: number = 0;

    /**
     * @param function callback
     * @param number threshold
     * @return function
     */
    public constructor(callback: () => void, threshold: number = 200): () => void {
        this.callback = callback;
        this.threshold = threshold;

        return this.run.bind(this);
    }

    /**
     * Executable function
     * 
     * @return void
     */
    public run(): void {
        const now: number = Date.now();
        const diff: number = now - this.lastTrigger;

        // Execute Immediately
        if (diff > this.threshold) {
            this.lastTrigger = now;
            this.callback();
        }

        // Cancel future event, if exists
        if (this.timeout !== 0) {
            clearTimeout(this.timeout);
            this.timeout = 0;
        }

        // Create future event
        this.timeout = setTimeout(this.callback, this.threshold);
    }
}


// Implementation
// ------------------------------------------------------

function myFunction() {
    console.log('This is an inclusive debounced function');
}

const event = new InclusiveDebounce(myFunction, 1500);

// Test 1: Run on document scroll
window.addEventListener('scroll', event);
Enter fullscreen mode Exit fullscreen mode

In the class above, an additional property to store timeout was added. The function run included additional code to cancel existing timeouts and re-schedule them in the event of an additional execution.

For a more cohesive solution, you could use the logic within the InclusiveDebounce class and wrap certain parts with conditionals to have both an exclusive or inclusive debounce method.

Happy debouncing.

Latest comments (1)

Collapse
 
adamellsworth profile image
Adam

Thanks for the breakdown between inclusive/exclusive. I always feel like I have to relearn this pattern every time it fits a solution lol.