Javascript throttle and debounce

Debounce vs Throttle: Definitive Visual Guide

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.
2 const 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

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 )

Ball vending machine

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?

  • 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

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?

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 buttonbutton>
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.

Stay in touch

Never miss a single post or a project announcement I make. Follow me on Twitter to stay in touch, ask a question, or just discuss different engineering topics together.

Источник

Читайте также:  Java method names characters
Оцените статью