Engineering

Debounce vs Throttle: Definitive Visual Guide

Introduction

When it comes to debounce and throttle developers often confuse the two. Choosing the right one is, however, crucial, as they bear a different effect. If you are a visual learner as myself, you will find this interactive guide useful to differentiate between throttle and debounce and better understand when to use each.

First things first

Throttling and debouncing are two ways to optimize event handling. Before we begin, let's take a moment to briefly revise the basics of events. In this article I'm going to use JavaScript in all examples, yet the conepts they illustrate are not bound to any specific language.

Event is an action that occurs in the system. In front-end development that system is usually a browser. For example, when you resize a browser window the "resize" event is fired, and when you click on a button the "click" event is. We are interested in events to attach our own logic to them. That logic is represented as a function that is called a handler function (because it handles the event). Such handler functions may handle a UI element update on resize, display a modal window upon a button click, or execute an arbitrary logic in response to any event.

In JavaScript you can react to events using event listeners. Event listener is a function that listens to the given event on a DOM element and executes a handler function whenever that event occurs. To add an event listener to an element (target) you should use the addEventListener function:

1target.addEventListener(eventName, handler, options)

Let's throw a ball!

Let's build a ball throwing machine. Our machine would have a button that, when pushed, throws a ball. To describe this cause-and-effect relation between the button click and a ball throw we can use addEventListener on our button element:

1const button = document.getElementById('button')
2
3button.addEventListener('click', function () {
4 throwBall()
5})

This reads as: whenever the button is clicked, execute the throwBall() function. The details of throwBall function are not important, as it represents any logic bound to an event.

Hinges are tightened and the screws are steady—let's put our ingenious invention to the test!

Ball vending machine

Press the red button of the machine.

Button clicked:0 time(s)
Event handler called:0 time(s)

Whenever we press the button we produce the "click" event, to which event listener reacts with the throwBall() function. In other words, one button click results into one handler function call and one ball being thrown.

By default, event handler function executes with 1-1 ratio to the event call.

There are cases when such direct proportion may become undesired. For instance, what if throwing a ball was an expensive operation, or we couldn't afford to throw more than 1 ball in half a second? In those cases we would have to limit the amount of times our handler function is being called.

Throttling and debouncing are two most common ways to control a handler function response rate to an event. Let's analyze each of them more closely by tweaking our ball machine.

Throttle

A throttled function is called once per N amount of time. Any additional function calls within the specified time interval are ignored.

Implementing throttle

1function throttle(func, duration) {
2 let shouldWait = false
3
4 return function (...args) {
5 if (!shouldWait) {
6 func.apply(this, args)
7 shouldWait = true
8
9 setTimeout(function () {
10 shouldWait = false
11 }, duration)
12 }
13 }
14}

Depending on the use case such simplified implementation may not be enough. I recommend looking into lodash.throttle and _.throttle packages then.

The throttle function accepts two arguments: func, which is a function to throttle, and duration, which is the duration (in ms) of the throttling interval. It returns a throttled function. There are implementations that also accept the leading and trailing parameters that control the first (leading) and the last (trailing) function calls, but I'm going to skip those to keep the example simple.

To throttle our machine's button click we need to pass the event handler function as the first argument to throttle, and specify a throttling interval as the second argument:

1button.addEventListener(
2 'click',
3 throttle(function () {
4 throwBall()
5 }, 500)
6)

Here's how our patched ball machine would work with the throttling applied:

Ball vending machine

Press the red button of the machine.

Button clicked:0 time(s)
Event handler called:0 time(s)
Throttle options

No matter how often we press the button a ball won't be thrown more than once per throttled interval (500ms in our case). That's a great way to keep our ball machine from overheating during the busy hours!

Throttle is a spring that throws balls: after a ball flies out it needs some time to shrink back, so it cannot throw any more balls unless it's ready.

When to use throttle?

Use throttle to consistently react to a frequent event.

This technique ensures consistent function execution within a given time interval. Since throttle is bound to a timeframe, a dispatched event handler should be ready to accept an intermediate state of event.

Common use cases for a throttled function:

  • Any consistent UI update after window resize
  • Performance-heavy operations on the server or client

Debounce

A debounced function is called after N amount of time passes since its last call. It reacts to a seemingly resolved state and implies a delay between the event and the handler function call.

Implementing debounce

1function debounce(func, duration) {
2 let timeout
3
4 return function(...args) => {
5 const effect = () => {
6 timeout = null
7 return func.apply(this, args)
8 }
9
10 clearTimeout(timeout)
11 timeout = setTimeout(effect, duration)
12 }
13}

For more complicated scenarios consider lodash.debounce and _.debounce packages then.

The debounce function accepts two arguments: func, which is a function to debounce, and duration, which is the amount of time (in ms) to pass from the last function call. It returns a debounced function.

To apply debouncing to our example we would have to wrap the button click handler in the debounce:

1button.addEventListener(
2 'click',
3 debounce(function () {
4 throwBall()
5 }, 500)
6)

While the call signature of debounce is often similar to the one in throttle, it produces a much different effect when applied. Let's see how our machine will behave if its button clicks are debounced:

Ball vending machine

Press the red button of the machine.

Button clicked:0 time(s)
Event handler called:0 time(s)
Debounce options

If we keep pressing the button fast enough no balls will be thrown at all, unless a debounce duration (500ms) passes since the last click. It is if our machine treats any amount of button clicks within a defined time period as a single event and handles it respectively.

Debounce is an overloaded waiter: if you keep asking him your requests will be ignored until you stop and give him some time to think about your latest inquiry.

When to use debounce?

Use debounce to eventually react to a frequent event.

Debounce is useful when you don't need an intermediate state and wish to respond to the end state of the event. That being said, you need to take into account an inevitable delay between the event and the response to it when using debounce.

Common use cases for a debounced function:

  • Asynchronous search suggestions
  • Updates batching on the server

Common problems

Re-declaring debounced/throttled function

One of the most common mistakes when working with these rate limiting functions is repeatedly re-declaring them. You see, both debounce and throttle work due to the same (debounced/throttled) function reference being called. It is absolutely necessary to ensure you declare your debounced/throttled function only once.

Allow me to illustrate this pitfall. Take a look at this click event handler:

1button.addEventListener('click', function handleButtonClick() {
2 return debounce(throwBall, 500)
3})

It may look fine at first, but in fact nothing is going to be debounced. That is because the handleButtonClick function is not debounced, but instead we debounce the throwBall function.

Instead, we should debounce an entire handleButtonClick function:

1button.addEventListener(
2 'click',
3 debounce(function handleButtonClick() {
4 return throwBall()
5 }, 500)
6)
Remeber that the event handler function must be debounced/throttled only once. The returned function must be provided to any event listeners.

React example

If you are familiar with React you may also recognize the following declaration as being invalid:

1class MyComponent extends React.Component {
2 handleButtonClick = () => {
3 console.log('The button was clicked')
4 }
5
6 render() {
7 return (
8 <button onClick={debounce(this.handleButtonClick, 500)}>
9 Click the button
10 </button>
11 )
12 }
13}

Since debounce is called during the render, each MyComponent's re-render will produce a new instance of a debounced handleButtonClick function, resulting into no effect being applied.

Instead, the handleButtonClick declaration should be debounced:

1class MyComponent extends React.Component {
2 handleButtonClick = debounce(() => {
3 console.log('The button was clicked')
4 }, 500)
5
6 render() {
7 return <button onClick={this.handleButtonClick}>Click the button</button>
8 }
9}

Finding optimal duration

With both debounce and throttle finding a duration time optimal for UX and performance is important. Choosing a fast interval will not have impact on performance, and picking a too long interval will make your UI feel sluggish.

The truth is, there is no magical number to this, as time interval will differ for each use case. The best advice I can give you is to not copy any intervals blindly, but test what works the best for your application/users/server. You may want to conduct A/B testing to find that.

Afterword

Thank you for reading through this guide! Of course, there's much more to events handling, and throttling and debouncing are not the only techniques you may use in practice. Let me know if you liked this article by reposting or retweeting it.

Special thanks to Alexander Fernandes for "Ball Bouncing Physics" project used for balls physics in the vending machine example.

Artem Zakharchenko

Artem Zakharchenko

@kettanaito

Hi! My name is Artem and I am a Full-stack JavaScript engineer, rock-n-roll musician and medical doctor.

If you like my material, please consider following me on Twitter to get notified when new posts are published, ask me a question and stay in touch.

Articles You May Enjoy