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.

The basics

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 concepts 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:

element.addEventListener(eventName, listener, 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:

1// Find a button element on the page.
2const button = document.getElementById('button')
3
4// And react to its click event.
5button.addEventListener('click', function () {
6 throwBall()
7})

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 test!

Ball vending machine
Button clicked:0 time(s)
Event handler called:0 time(s)

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

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

There are cases, however, when such a 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 listener is being called.

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


Throttle

Throttling is the action of reducing the number of times a function can be called over time to exactly one.

For example, if we throttle a function by 500ms, it means that it cannot be called more than once per 500ms time frame. Any additional function calls within the specified time interval are simply 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, this simplified implementation may not be enough. I highly 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
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 throttling to consistently react to a frequent event.

This technique ensures consistent function execution within a given time interval. Since throttle is bound to a fixed time frame, the event listener should be ready to accept an intermediate state of the event.

Common use cases for throttling include:

  • 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
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 clickeds')
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.