Debounce: How to optimize search in a Web Application

Debounce: How to optimize search in a Web Application

Before learning any concept I usually dig into the problem that it solves. So, let's discuss the problem.

Problem

Assume that you are a Marvel fan (like me) and you are making a website that will list all the Marvel characters and their details. Now, most probably you'll use a card-like structure that has basic details like the image of the character, name, powers, team affiliation.

Now for anyone to use your website to get the details of their favorite marvel character they'll need a search bar where they can enter the character's name and get the result.

Let's assume that there is a marvel API (there actually is) that provides you with the list of all the marvel characters and also has a search endpoint where you can pass the search query string and get all the characters data that match the query string.

Now you just add an "onChangeEventListener" to your search input and make a call to the search API on each entry to the input. But this will cause unnecessary API calls to the server and unnecessary renders in your UI till the user has typed the exact marvel character name he is looking for.

For example, if he enters "Spiderman", there will be 9 API calls and 9 renders in your UI for each time that API returns that filtered data.

Here we are considering an example of an external API, what if the number of API calls by your API key for a day is restricted then the user will unknowingly waste many API calls. Also what if the API is created by you on a server. In each API call, the backend code has to filter characters from thousands of marvel characters and return the data. The API calls are expensive.

Solution

  1. Hava a submit button named 'search' which the user will click after he/she is done entering the input. This solution works fine but is not an ideal User experience.

  2. Debounce the search input. This is what we will discuss further.

Debounce - The Concept

Debounce is when we handle the event only after a certain period of time (usually microseconds) post the event stops firing repeatedly.

So considering the above example we want to handle the 'onChangeListener' only after a few microseconds after the user has stopped entering input, indicating that the user is now waiting for the result.

What I explained above, is just one of the issues that are solved using debounce. This concept can be applied to any event listener that is provided by the DOM API.

Check below for the implementation of Debounce as a Javascript function.

Debounce implementation in Javascript

const handleOnChangeEvent = (event) => {
    // make API call to fetch filtered result and update UI
}

Above is the function that we want to debounce which makes the API call to fetch the filtered results

const debounce = (fn, delay = 100) => {
    return () => {
        setTimeout(() => {
             fn()
        }, delay);
    }
}

const optimizedEventHandler = denounce(handleOnChangeEvent, 300);

The debounce function takes two arguments, first is the function that handles the event and the second is the delay we want after which the event handler should be called. In the above code, I have set the delay as 100ms by default.

This debounce function returns a function that contains the setTimeout which will cause the delay in calling the event handler.

Now consider the case when the delay is 500ms and the user starts typing again within the 500ms. Given the above code, there will be 2 calls to the event handlers. But the first call is now not needed. The following code will take care of this case.

const debounce = (fn, delay = 100) => {
    let timer;
    return () => {
        clearTimeout(timer);
        timer = setTimeout(() => {
             fn()
        }, delay);
    }
}

const optimizedEventHandler = denounce(handleOnChangeEvent, 300);

To understand what we did now needs the understanding of closure (which I will cover in detail in another post ). Simply put, the returned function has access to the timer variable which is declared in its parent's scope. Here we clear the timeout in case the user starts typing again within the given delay hence preventing the stale event handler function call.

I hope this post helped you understand the concept of debouncing and its implementation. Feel free to reach out to me if you have doubts.