Beyond the Keystrokes: Optimizing Real-Time Suggestions with Debounce
Tags: performance, webdev, javascript, programming
Published: 2023-08-27 13:32:50

Recently, I had a task to update a form feature at my work. It’s just a regular multi-step form we all used to see. But, what’s interesting for me is one of the input fields behaves like a Combobox. So, when a user types in the input field, there will be a suggestion box that appears below the field.

![Flow chart of a simple Combobox](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hhz0wn83otvqy6v1mj8j.png)

The concept is similar to the search bar of Instagram. When the user types into the search bar, there will be an API call to the backend service to retrieve a small subset of usernames with the highest similarity to the user input and then display it back to the user.

## Potential Problems

The most straightforward approach for this feature may be just to create a function called `handleChange` that calls the suggestions backend service and then attaches it to the `onChange` listener. For simple applications, this implementation is perfect and serves its purpose. But, for more complex applications that require a lot more computation and a gigantic database to query from, this implementation will eventually give us some problems.

```jsx
async function handleChange(e) {
	const users = await callSomeAPI(e.target.value);  // This will take awhile ⌛
	showUserSuggestions(users);
}
```

```html
<input type="text" onChange="handleChange" />
```

Firstly, with the current implementation, we will call handleChange again and again immediately on every single keystroke. If there’s a heavy computation inside the function, congratulations, we just block the main thread and potentially freeze the UI. No one likes freeze UI.

![A simplified state diagram of a text input](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i7g329pzodkiob4hq4gx.png)

On top of that, we will make multiple API calls to the backend. This will affect the backend and also users’ network data. For me, reducing data transfer costs is also a part of web accessibility. Since it opens access for more people using the feature. In my country, Indonesia, internet speed varies across the archipelago, and for some people, the cost of internet access is also considered quite expensive. Therefore, excessive network request is an aspect that deserves attention.

## What should we do then?

One of the first potential solutions is to call the function call only when it reaches a certain input length. For example, we don’t want to start showing the suggestion unless the user types 3 characters or more. Thus, no matter how many times a user types, as long as it doesn’t exceed 2 characters, no network request.

```jsx
async function handleChange(e) {
	if (e.target.value < 3) return;  // Act as a safe guard
	const users = await callSomeAPI(e.target.value);
	showUserSuggestions(users);
}
```

But, if we think about it, this doesn’t really solve the problem, right? Because it will still invoke the function every time after the input length exceeds 2 characters. So, are there other alternative?

### Introducing Debounce

Debounce is a software mechanism to prevent invoking a slow function too often. There are many possible applications of debounce. Deboune is also known in hardware engineering, but we won’t discuss it here. 

The idea of debounce is to invoke a function only after a certain amount of time has passed without any new function call. So, let’s say we want to make `handleChange` use debounce, and we set the time needed to pass at 500 milliseconds. This means we can call `handleChange` as much as we want, but as long as the time gap between function call is less than 500 milliseconds, the program will ignore all of it and only invoke it after 500 milliseconds has passed without any new function call.

![Normal function call timeline](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h3anzpjdmjgjp2y9uok1.png)

Let's break down the process with a practical example. Imagine a user typing into an input field, and they begin by typing "Java" quickly. If they pause for less than 500 milliseconds and then stop typing for a full second, the **`handleChange`** function is called once. At this point, the value of **`e.target.value`** will be "Java".

Now, let's explore a scenario where, after that 1-second pause, the user starts typing "S", briefly pauses for 500 milliseconds, types "c", pauses again for 500 milliseconds, types "ript", and finally stops typing. In this case, the **`handleChange`** function is invoked three times: first with the value "S", then "c", and finally "ript".

![Debounced function call timeline](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fsanrvp7b2a3m3lcvaac.png)

The real beauty of this lies in its efficiency. Consider a word that's 10 characters long. Instead of triggering **`handleChange`** 10 times as it normally would, the debouncing technique steps in and streamlines the process, resulting in just 4 invocations. This is a huge win!

### Implementation in Javascript

Now that we've mastered the concept of debouncing, let's dive into its implementation using everyone's favorite tool: JavaScript! To achieve debouncing in JavaScript, we'll leverage a nifty trick involving closures and the **`setTimeout`** function. Here's how you can cook up your very own debounced function:

```jsx
function debounce(func, delay) {
  let timeoutId;

  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}
```

In this snippet, our **`debounce`** function takes two properties: the **`func`** (the function you want to debounce) and the **`delay`** (the time in milliseconds to wait before invoking the debounced function). Using a closure, we manage the **`timeoutId`** so we can clear the timeout if the user keeps typing.

And now, just pass `handleChange` inside `debounce` like so:

```jsx
async function handleChange(e) {
	const users = await callSomeAPI(e.target.value);
	showUserSuggestions(users);
}

const handleChangeDebounced = debounce(handleChange, 500);
```

Picture this: as users furiously type into the input field, our function remains calm and collected. It only springs into action if the typing pauses for more than 500 milliseconds, ensuring that we're not sending unnecessary requests and bombarding the server. We finally found a working solution to our initial problem.

## Wrapping Up

And there you have it, the saga of transforming a clunky text input into an accessible and network-friendly one! Debounce is only one of many alternative solutions we can choose. And just like any other performance tool out there, we need to use it wisely and avoid premature optimization.

![Thumbs up meme gif](https://media.giphy.com/media/111ebonMs90YLu/giphy.gif)

This is the first blog post I have written here, please give your feedback and suggestions. Whether it is about my writing, illustration, or anything else, it will be really valuable for me. I hope you can get some valuable insights from my experience! Thanks for reading 🎉