
In optimizing a product’s performance, engineers end up designing a process that does the most with the least. This either by finding a way to minimize an application’s tasks to give the same output or the reduce the cost of performing a task.
In client-facing applications, whenever a user performs an activity on the application (like typing, clicking, resizing the window, etc), it probably emits some kind of event. So, as repeatedly as the user performs a particular action, the application also fires the same events and executes the functions we wrote to handle those events, making HTTP calls too. This means using more resources and increasing costs.
Now we cannot or I should say should not stop or hinder the user’s ability to perform an action to optimize the performance but rather implement a technique to filter out or minimize the number of times a function gets called and keep the user experience the same.
Debounce and Throttle are two such optimization techniques we use on events that can be fired more often than you need them to.
Debouncing enforces that a function not be called again until a certain amount of time has passed without it being called. For example, “execute this function only if 100 milliseconds have passed without it being called.”
Throttling enforces a maximum number of times a function can be called overtime. For example, “execute this function at most once every 100 milliseconds.”
Confusing? Let’s take a look at them one at a time and understand the use cases and implementation. It will make sense.
Debounce
Let’s take a real-life example and forget about coding for now. So let’s say you are at your job working and also chatting with a friend over a messaging app who is telling you a story in bits and pieces so you are getting notifications constantly. But you can’t just wait there and read each message as it comes because you have to be at work also. So what you do is ignore all the messages and check all the messages and reply once when your friends stop sending messages. This is debouncing.
In debounce, we keep on delaying the execution of a function till it’s being called again and again. And then finally execute it. In this way, we group all the multiple sequential calls and execute them once.
A very popular scenario is when we want to show the search results for the typed-in keywords by the user as the user types, dynamically. So if we call the API to get the search results on each key event then there would be as many calls as the changes in the search input field. So by using debounce we only call the function to get the results from API when the user stops typing for let’s say 500 milliseconds.
Implementation
Vanilla JS
index.html
<body>
<input onkeyup="console.log('keyup'); onKeyupHandler(event.target.value)">
<script src="./index.js"></script>
</body>
index.js
function getData(searchTerm) {
console.log('api call for', searchTerm)
};
const debounce = function (fn, delay) {
let timer;
return function() {
let args = arguments;
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args);
}, delay)
}
}
const onKeyupHandler = debounce(getData, 500)
Outcome
React.js
import React from 'react';
const debounce = (fn, d) => {
let timer;
return function () {
let context = this
let args = arguments
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args)
}, d)
}
}
const InputComponent = ({label, changed}) => {
const getData = (value) => {
console.log('api call for', value);
}
const handleOnChange = React.useCallback(debounce((value) => getData(value), 1000), [])
return (
<div>
<label>{label} : </label>
<input onChange={(event) => handleOnChange(event.target.value)}/>
</div>
)
}
export default React.memo(InputComponent);
Angular
@Component({
selector: 'app-search-bar',
Template: `
<input #searchField placeholder="Search" type="text">
`,
})
export class SearchBarComponent implements AfterViewInit {
@Input() searchDebounceDelay: number = 500;
@ViewChild('searchField', { static: false }) searchField: any;
ngAfterViewInit(): void {
fromEvent(this.searchField.nativeElement, 'keyup')
.pipe(
debounceTime(this.searchDebounceDelay),
distinctUntilChanged(),
map(x => x['target']['value'])
)
.subscribe(value => {
// api call to get the search result
console.log(value)
}
Common Use Cases for Debounce
- Search bar
- Batch updating
- Validating form data entered and showing error.
Throttle
Take the same example of you and your friend chatting but this time you decide that no matter how many times the notification comes you will check and reply to the messages after every 5 minutes.
In throttle, we execute the function the first time we listen to the event and then ignore it for a fixed interval and then execute the function. So in this way we limit the number of function executions in an interval of time.
Another popular feature is infinite scroll where the app keeps on adding more list items as the user scrolls down. If we listen to a scroll event it can be fired 100 or maybe 1000 times and the frequency of the event fired also depends on how fast we scroll. Now no way we want to call the API that way.
By using throttle we can execute the method that gets the request after a fixed interval while the user is scrolling.
Implementation
Vanilla JS
index.html
<body>
<input onkeyup="console.log('keyup'); onKeyupHandler(event.target.value)">
<div style="height: 50vh; border-bottom: 10px solid black;"></div>
<div style="height: 50vh; border-bottom: 10px solid black;"></div>
<div style="height: 50vh; border-bottom: 10px solid black;"></div>
<div style="height: 50vh; border-bottom: 10px solid black;"></div>
<div style="height: 50vh; border-bottom: 10px solid black;"></div>
<div style="height: 50vh; border-bottom: 10px solid black;"></div>
<div style="height: 50vh; border-bottom: 10px solid black;"></div>
<div style="height: 50vh; border-bottom: 10px solid black;"></div>
<div style="height: 50vh; border-bottom: 10px solid black;"></div>
<script src="./index.js"></script>
</body>
Index.js
function getData(page) {
console.log('api call for', page)
};
const throttle = function (fn, delay) {
let timer;
return function() {
let args = arguments
if (timer) {
return
} else {
fn.apply(this,args);
timer = setTimeout(() => {
clearTimeout(timer)
timer = null
}, delay)
}
}
}
const onKeyupHandler = throttle(getData, 500)
const scrollDownHandler = throttle(getData, 500)
let lastScrollTop = 0
document.addEventListener('scroll', function(e) {
let scrollY = window.scrollY;
if (scrollY > lastScrollTop) {
console.log(‘scrolled’)
scrollDownHandler('more data')
}
lastScrollTop = scrollY;
})
React.js
import React from 'react';
import ListItem from "./list-item.component";
export const throttle = function (fn, delay) {
let timer;
return function() {
let args = arguments;
if (timer) {
return
} else {
fn.apply(this, args);
timer = setTimeout(() => {
clearTimeout(timer)
timer = null
}, delay)
}
}
}
const ParentComponent = () => {
const [lastScrollTop, setLastScrollTop] = React.useState(0);
const getData = (value) => {
console.log('api call for', value)
}
const scrollDownThrottle = React.useCallback(throttle(getData, 2000), [])
const handleOnScroll = (e) => {
let scrollY = window.scrollY;
if (scrollY > lastScrollTop) {
scrollDownThrottle(‘next page');
}
setLastScrollTop(scrollY);
}
React.useEffect(() => {
window.addEventListener('scroll', handleOnScroll)
}, [])
return (
<div>
{
[1, 2, 3, 4, 5].map(x => <ListItem/>)
}
</div>
)
}
export default React.memo(ParentComponent);
Angular
export class AppComponent implements OnInit {
private resizeSubject = new Subject<number>();
private resizeObservable = this.resizeSubject.asObservable()
.throttleTime(200);
@HostListener('window:resize', ['$event.target.innerWidth'])
onResize(width: number) {
this.resizeSubject.next(width);
}
ngOnInit() {
this.resizeObservable.subscribe(x =>
this.doSomethingWithThrottledEvent(x));
}
private doSomethingWithThrottledEvent(width: number) {
// . . .
}
}
Whether to throttle or debounce?
Both these techniques look kinda similar, almost like brothers to each other and it’s quite normal to be confused in what is the difference between the 2 or thinking when to use what. I’ll answer this for you.
And the answer is…
Drum roll !
It depends. : P
No but really, it depends on the use case that you have to solve.
So like if we take the example of infinite scroll and instead use debounce. Then the function will execute when the user stops scrolling but we don’t allow the user to reach the end of the page and make him/her wait for a few seconds and then load more posts.
And if for the search bar we implement throttle then instead of loading the search results when the user stops typing we will be loading the search results while the user is typing at a fixed interval.
Example scenario
In search field : samsung mobiles { user types ‘samsung’ in one go; pauses then types ‘mobile’ }
Debounce:
Gets result for ‘samsung’
Gets result for ‘samsung mobile’
Throttle:
Gets result for ‘sam’
Gets result for ‘samsung’
Gets result for ‘samsung mob’
Gets result for ‘samsung mobile’
So to me, it makes more sense to let the user type in, and then we show the search for what was really intended by the user instead of showing results for loading results for incomplete search terms.
So it really depends on the use case and how your teams want the user experience to be.
When we are concerned about the initial or the intermediate states of the event we should use the throttle. And When we are concerned about only the final stage of the event we should use debounce.
Conclusion
Every web developer must be aware of these techniques and use them in practice because it’s super important to optimize the application’s performance. Understanding the use case and how we want the user experience to be we can make a good judgment on which one to use. A lot of good libraries are available to implement these techniques like RxJS, Lodash.
I hope some of you find this technique as useful as it’s been for me.
More related to web application optimation:
https://blog.knoldus.com/web-application-optimization-cases-tips-tricks-tools/