A few months ago, my manager assigned me a task: implement a search functionality across an entire page. The tricky part was that the displayed text was shown in the form of prompts, and each prompt could be truncated after two lines.
If the text exceeded the limit, a split button appeared, allowing users to open the full prompt in a separate split-view section (see the illustration below for better understanding).
Now, if the requirement had been just plain text, I could have solved it with a simple regex-based search. In fact, inside the split view itself, I initially used a regex approach for searching along with navigation to the matches. That worked fine.
Since I already had a working search helper function, I thought, “Why not reuse it for the global search as well?”
Well, I tried that. But this time, the UI started lagging whenever I clicked the next/previous buttons in the search bar. Even the pagination controls on the top-right slowed down during navigation. To figure out a better approach, I turned to AI tools for brainstorming and came across multiple ideas and concepts.
As developers, we use Google daily, and naturally, I became curious about how Google’s search works under the hood. I opened up Chrome DevTools, started typing in Google’s search bar, and noticed something interesting.
While Google Search updates results in real-time with each keystroke, we don’t have Google’s server power. In our apps, debouncing is a practical way to avoid unnecessary API calls and improve performance. That idea matched exactly what ChatGPT had suggested to me earlier.
So, I applied a similar approach to my project and finally delivered the feature using debouncing, along with React hooks like useTransition
and useDeferredValue
. That’s how the idea for this article came out.
In this article, I’ll show you how to optimize your application’s performance by implementing the debounce technique.
Table of Contents
Let’s dig in.
Problem Without Debouncing
Imagine you’re building a search bar that fetches results from an API. Every time the user types a letter, the search bar immediately makes a new request.
If someone types the word “JavaScript”, that means 10 separate API calls will be fired — one for each character.
Now, in Google Search, results update in real-time with every keystroke. But unlike Google, we don’t have massive infrastructure to handle that load. In most applications, firing a request for every single character quickly becomes inefficient.
At first, this might not seem like a big deal, but in practice it leads to serious problems. The browser has to manage a flood of unnecessary requests, the server gets overloaded with repeated calls, and the user ends up with a laggy or inconsistent experience. The whole interface feels heavy and unresponsive.
This is exactly the situation I ran into when I reused my simple regex-based search function for the global search. It worked fine for a small prompt inside the split view, but when applied at a larger scale with navigation buttons and pagination, the UI started freezing and slowing down.
What is Debouncing?
Debouncing is a technique, not a programming language feature. It’s simply a way to control how often a function gets called. Instead of running the function every single time an event happens, you delay its execution.
If the event keeps firing during that delay, the timer resets. The function only runs when the user finally pauses.
Think about typing into a search bar. Without debouncing, the app would make a request for every keystroke. With debouncing, the app waits until the user stops typing for a short time—say 300 milliseconds and then makes just one request with the final input.
Behind the scenes, this is usually implemented with setTimeout
and clearTimeout
. A timer starts when the event occurs, and if another event happens before the timer ends, the timer is cleared and restarted. Only when the user stops typing for the specified delay does the function execute.
How to Implement Debouncing in JavaScript
As I mentioned earlier, debouncing is not tied to any specific programming language. It’s simply a concept that can be implemented using timers. In JavaScript, we typically use setTimeout
and clearTimeout
to achieve this.
Here’s a simple example of a debounce function in JavaScript:
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
We start with a function debounce
that takes two arguments:
fn
is the function we want to control, such as the API call.delay
is how long we want to wait before actually runningfn
.
Inside, we declare a variable timer
. This will hold the reference to the setTimeout
.
The debounce
function then returns another function. This returned function is the one that will actually run whenever an event (like typing in the input or API call) happens.
Every time the user types, the first thing you do is clearTimeout(timer)
. This cancels any previously scheduled function call. Then you create a new timeout with setTimeout
.
If the user keeps typing before the delay finishes, the old timer is cleared and restarted. Only when they pause long enough does the timeout finish and fn
gets executed.
Did you notice how I used fn.apply(this, args)
? That’s just a safe way of calling the original function with the correct this
context and passing along all arguments.
Now here’s how you use it in practice:
function fetchResults(query) {
console.log("Fetching results for:", query);
// Here you could call your API
}
// Wrap it with debounce
const debouncedSearch = debounce(fetchResults, 300);
// Attach to input event
const input = document.getElementById("search");
input.addEventListener("input", (e) => {
debouncedSearch(e.target.value);
});
fetchResults
is our actual search function. Normally, it would run for every keystroke.We wrap it with
debounce
and set a delay of 300ms. That means it won’t run until the user stops typing for 300ms.On every
input
event, instead of callingfetchResults
directly, we calldebouncedSearch
. This ensures only the debounced version of the function executes.
So if a user types “hello,” instead of five API calls, only one or two will fire once they pause.
Benefits of Using Debouncing in Search
Using debouncing in a search feature may feel like a small optimization, but it has a big impact. The most obvious benefit is performance.
Instead of making a request for every single keystroke, your app waits until the user pauses, which saves both browser and server resources. The UI feels much smoother because it isn’t constantly being interrupted by unnecessary calls.
Debouncing also improves scalability. If hundreds or thousands of users are typing at once, you’re cutting down a huge number of wasted API calls. That means your backend can handle more users without getting overloaded.
There’s also an indirect benefit for SEO and analytics. When your app performs efficiently and feels snappy, users stay longer, interact more, and bounce less. This kind of responsiveness can make a big difference in how people perceive the quality of your product.
Common Mistakes to Avoid
While debouncing is powerful, there are a few mistakes developers often make. One common issue is setting the delay too high. If you make users wait one or two seconds before seeing results, the search will feel unresponsive.
On the other hand, a delay that’s too short may not reduce calls enough to be useful. A sweet spot is usually between 300–500 milliseconds, but it depends on your use case.
Another mistake is forgetting to clear old timers. Without clearing, your app may still execute older, outdated calls, which can lead to glitches or memory leaks. That’s why clearTimeout
is just as important as setTimeout
in any debounce function.
It’s also important to think about edge cases. What happens if the input is cleared quickly? Or if someone pastes a long string instead of typing? Testing these cases ensures your debounce function works smoothly in real-world scenarios.
Conclusion
When I first encountered the challenge of building a global search, I thought I could simply reuse my basic regex-based solution. However, the UI soon began to lag, and the user experience declined. It's surprising how such a small concept can significantly impact performance.
Debouncing ensures that your functions run at the right time, not every time. Whether you’re building a simple JavaScript app or working with React and Next.js, this technique helps reduce unnecessary calls, improves performance, and keeps your app scalable.
So the next time you build a search bar, remember: don’t just make it work, make it efficient.
Before We End
I hope you found this article insightful. I’m Ajay Yadav, a software developer and content creator.
You can connect with me on:
Twitter/X and LinkedIn, where I share insights to help you improve 0.01% each day.
Check out my GitHub for more projects.
I also run a Hindi YouTube Channel where I share content about careers, software engineering, and technical writing.
See you in the next article — until then, keep learning!