Brad Woods Digital Garden

Notes / JavaScript / Performance / Debounce and throttle

The Warhammer 40k Adeptus Mechanicus symbol

Table of contents

    A chart comparing triggered events, debounced events and throttled events over time

    Debounce and throttle

    Planted: 

    Tended: 

    Status: mature

    Hits: 332

    Intended Audience: Front-end developers

    Debounce and throttle are two techniques to improve performance and UX (User Experience). Both limit how many times a function is invoked over a period of time.

    MOVE MOUSE HERE

    Events over time

    Debounced events

    Throttled events

    Debounce

    A debounce function takes a function and returns an optimized version of it. The optimized version groups a sudden burst of function calls into one.

    How it works. When debouncedFunc is invoked, a timer will start. When the timer completes, func is invoked. If, debouncedFunc is invoked again before the timer completes, the timer will restart.

    index.js

    function debounce(callback, waitMS = 200) {
    let timeoutId;
    return function(...args) {
    const context = this
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
    timeoutId = null
    callback.call(context, ...args)
    }, waitMS);
    };
    };
    function func(x) {
    console.log(x);
    }
    const debouncedFunc = debounce(func)
    // Will be called
    debouncedFunc(1);
    // Won't be called because of debouncing
    debouncedFunc(1);
    // Will be called because it is called after the debounce limit has expired from the initial call above
    setTimeout(() => debouncedFunc(1), 200);

    There are two types of debounce implementations:

    • Trailing edge: The above implementation. func is invoked AFTER the timer has completed.
    • Leading edge: func is invoked BEFORE the timer starts. When the timer completes, func will not be invoked. Implementation below.

    index.js

    function debounce(callback, isLeadingEdge = false, waitMS = 200) {
    let timeoutId;
    return (...args) => {
    const context = this
    const isCallNow = isLeadingEdge && !timeoutId
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
    timeoutId = null
    if (!isLeadingEdge) {
    callback.call(context, ...args)
    }
    }, waitMS);
    if (isCallNow) {
    callback.call(context, ...args);
    }
    };
    };
    function func(x) {
    console.log(x);
    }
    const debouncedFunc = debounce(func, true)
    // Will be called
    debouncedFunc(1);
    // Won't be called because of debouncing
    debouncedFunc(1);
    // Will be called because it is called after the debounce limit has expired from the initial call above
    setTimeout(() => debouncedFunc(1), 200);

    Use case. Input validation. Imagine a form with an email input. Each time the user enters a character, a validation function is invoked. If the input value is not in a valid email format, it renders a error message. This UX isn't great. An error message is shown as soon as the first character is entered. To improve. Debounce the validation function. The validation function will only be invoked after the user has finished entering all characters of their email.

    Throttle

    A throttle function takes a function and returns an optimized version of it. The optimized version prevents a function being called more than once every x milliseconds.

    How it works. When throttledFunc is invoked, func will be invoked and a timer will be started. Until the timer completes, any calls to throttledFunc will not invoke func.

    index.js

    function throttle (func, waitMS = 200) {
    let isWait = false;
    return function(...args) {
    if (!isWait) {
    func.call(this, ...args);
    isWait = true;
    setTimeout(() => {
    isWait = false;
    }, waitMS);
    }
    }
    }
    function func(x) {
    console.log(x);
    }
    const throttledFunc = throttle(func)
    // Will be called
    throttledFunc(1);
    // Won't be called because of throttling
    throttledFunc(1);
    // Will be called because it is called after the throttle limit has expired from the initial call above
    setTimeout(() => throttledFunc(1), 200);

    Use case. Window scroll callback. Imagine you set a callback for the window scroll event. It could be invoked up to 30 times per second. Depending on what the callback was doing, a lot of these could be redundant. While the browser is processing all them, it could block execution, making the browser unresponsive. Throttling the callback will reduce the number of invokes.

    requestAnimationFrame

    window.requestAnimationFrame is a method provided by the browser that will throttle a function. It requests the browser invokes a function before the next repaint. requestAnimationFrame can be thought of as a throttle with waitMS = 16 (60fps). However, internally will decide the best timing on how to schedule the rendering. It has a much higher fidelity than a common throttle being a browser native API that aims for better accuracy.

    When a property like opacity is changed, the user won't see that change until the browser does a repaint. Imagine you have an element with an opacity of 1. You create a function that change that element's opacity. You invoke the function twice, once to change it to 0.9, then again the change it to 0.8. If both of those calls occur before the browser does a repaint, then the 1st will have no effect. The user will only see the element change from 1 to 0.8. These unrequired invokes can be avoided using requestAnimationFrame.

    How it works. You pass a function to requestAnimationFrame. It won't be invoke until the browser is ready to make the next repaint.

    Use case. When a function is painting or animating properties and you want to guarantee smooth changes. For example, changing the opacity of an element based on the scroll position. Below, we use requestAnimationFrame when a new value for opacity needs to be set. If that value changes before a repaint, we cancel the request and set a new one with the latest value.

    .call(..)

    In the above examples, callback.call(..) is used instead of callback(..). This is to ensure the correct this value is bound. See JavaScript - this for details.

    Feedback

    Have any feedback about this note or just want to comment on the state of the economy?

    Where to next?

    JavaScript
    Performance
    Arrow pointing downYOU ARE HERE
    A Johnny Cab pilot