Left ArrowBack

notes / JavaScript / performance / debounce throttle

debounce throttle

A chart comparing triggered events, debounced events & throttled events over time.

Debounce & Throttle

Planted

Status: seed

Debounce & throttle are 2 techniques to improve performance & / or UX (User Experience). They limit how many times a function is invoked over a period of time.

M

etaphor:

A bouncer

Imagine a function is a nightclub. Invoking the function is people entering the club. Debouncing or throttling a function is adding a bouncer to the club's front door. They decide who comes in & who doesn't.

If the function is debounced, the bouncer will make everyone that turns up to wait in line. Then, after 5 minutes, everyone in line can come in at once. If the function is throttled, the bouncer will let the 1st person who shows up in. If anyone else shows up in the next 5 minutes, they will be turned away.

MOVE MOUSE HERE

Events over time

Debounced events

Throttled events

A chart comparing triggered events, debounced events & throttled events over time. Debounced circled.

Debounce

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

How It Works

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

/index.js

Console

function debounce(callback, waitMS = 200) {
   let timeoutId;
	
   return function(...args) {
      const context = this
      clearTimeout(timeoutId);

      timeoutId = setTimeout(function(){
        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 2 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

Console

function debounce(callback, isLeadingEdge = false, waitMS = 200) {
   let timeoutId;
	
   return function(...args) {
      const context = this
      const isCallNow = isLeadingEdge && !timeoutId

      clearTimeout(timeoutId);

      timeoutId = setTimeout(function(){
        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 will be shown as soon as the 1st character is entered into the input. An improvement would be to instead invoke the validation function only after the user has finished entering all characters of their email address. This can be done by debouncing the validation function.

A chart comparing triggered events, debounced events & throttled events over time. Throttled circled.

Throttle

A throttle function takes a function & 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 & a timer will be started. Until the timer completes, any calls to throttledFunc will not invoke func.

/index.js

Console

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 redundant. While the browser is processing all them, it could block execution, making the browser unresponsive. Throttling the callback would 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 & 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 & set a new 1 with the latest value.

localhost:3000

Console

const p = document.querySelector('p')
const div = document.querySelector('div')

function updateDOM(lastScrollY) {
   const divHeight = div.getBoundingClientRect().height
   const viewportHeight = window.innerHeight;
   const scrollFraction = lastScrollY / (divHeight - viewportHeight);

   p.style.opacity = 1 - scrollFraction
}

function rAFThrottle(callback) {
   let requestID

   return function(...args) {
      const context = this

      cancelAnimationFrame(requestID)

      requestID = requestAnimationFrame(() => {
         callback.call(context, ...args)
         isWait = false
      })
   }
}

const throttledUpdateDOM = rAFThrottle(updateDOM)

function onScroll() {
   throttledUpdateDOM(window.scrollY)
}

window.addEventListener('scroll', onScroll)

Above, viewportHeight is being calculated unnecessarily every update. It couldn't be moved outside of the function. Possibly related to iframe loading delay.

.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 more details.

Where to Next?

A sci-fi robot taxi driver with no lower body