How to Optimize web app with Debounce and Throttle

Knoldus Blog Audio
Reading Time: 7 minutes

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/

Leave a Reply