DEV Community

Cover image for Beyond the Keystrokes: Optimizing Real-Time Suggestions with Debounce
Raffi Zulvian Muzhaffar
Raffi Zulvian Muzhaffar

Posted on

Beyond the Keystrokes: Optimizing Real-Time Suggestions with Debounce

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

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.

async function handleChange(e) {
    const users = await callSomeAPI(e.target.value);  // This will take awhile ⌛
    showUserSuggestions(users);
}
Enter fullscreen mode Exit fullscreen mode
<input type="text" onChange="handleChange" />
Enter fullscreen mode Exit fullscreen mode

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

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.

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

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

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

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:

function debounce(func, delay) {
  let timeoutId;

  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}
Enter fullscreen mode Exit fullscreen mode

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:

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

const handleChangeDebounced = debounce(handleChange, 500);
Enter fullscreen mode Exit fullscreen mode

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

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 🎉

Top comments (0)