JavaScript Debouncing

In the previous lesson, we learned how the event loop schedules code and runs timers like setTimeout. Now let’s use that knowledge to control how often a function runs with a technique called debouncing.

⏳ What is Debouncing?

Debouncing delays running a function until the user stops triggering it for a set amount of time. If the event keeps firing, the timer keeps resetting, so the function runs only once the activity pauses.

Some events fire very rapidly. Typing in a search box fires on every keystroke, and resizing a window fires dozens of times a second. Running expensive work on each one wastes time and can flood a server with requests. Debouncing waits for a quiet moment, then runs the function a single time.

Event How often it fires What debouncing does
Typing in a search box Once per keystroke Calls the API after typing stops
Resizing the window Many times per second Recalculates the layout once resizing stops
Clicking a save button Once per click Saves after the last click in a burst

🛠️ Building a Debounce Function

A debounce function wraps your original function and returns a new one. The new function starts a timer, and every fresh call cancels the previous timer before starting a new one. The wrapped function runs only when a timer is allowed to finish.

debouncing.js
function debounce(callback, delay) {
let timerId;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
callback(...args);
}, delay);
};
}

Let’s walk through what each line does, reading from the inside out:

  • let timerId; declares a variable in the outer function that holds the id of the currently scheduled timer. It starts empty and gets reused on every call.
  • return function (...args) { ... } returns a new wrapped function. The ...args collects whatever arguments the caller passes, such as the event object from an event handler.
  • clearTimeout(timerId) cancels any timer that is still waiting. This is what stops a previous call from firing when a new call arrives.
  • timerId = setTimeout(() => { ... }, delay) schedules a new timer for delay milliseconds and saves its id in timerId, so the next call can cancel it.
  • callback(...args) runs the original function once the timer finally finishes, passing along the saved arguments. Only the last timer in a burst survives to reach this line.

The timerId variable lives in the outer function but is used inside the returned function. That is a closure: the inner function remembers timerId between calls, which is what lets each call cancel the one before it.

Why clearTimeout is the key

Without clearTimeout, every call would schedule its own timer and they would all fire. Canceling the old timer is what makes the function run just once after the activity stops.

🔍 A Search Input Example

Here is the classic use case. A search input should only call the API after the user stops typing for 300 milliseconds, instead of on every keystroke.

debouncing.js
const searchInput = document.querySelector("#search");
function searchApi(query) {
console.log("Calling API with:", query);
// fetch(`/api/search?q=${query}`)...
}
const debouncedSearch = debounce((event) => {
searchApi(event.target.value);
}, 300);
searchInput.addEventListener("input", debouncedSearch);

Let’s trace how this wires up to the input:

  • const searchInput = document.querySelector("#search") grabs the search box from the page so we can listen to it.
  • function searchApi(query) { ... } is the expensive work we want to limit. Here it just logs, but in real code it would call fetch.
  • const debouncedSearch = debounce(..., 300) calls debounce once and stores the returned wrapper. The 300 is the delay in milliseconds.
  • The inner arrow function reads event.target.value to get the current text and hands it to searchApi.
  • searchInput.addEventListener("input", debouncedSearch) runs debouncedSearch on every keystroke, but the wrapper resets the timer each time.

Every keystroke fires the input event, but debouncedSearch keeps resetting the timer. The moment the user pauses for 300 milliseconds, searchApi runs once with the final value. Typing “hello” sends a single request instead of five.

Picking a delay

Around 250 to 400 milliseconds feels responsive for search. Too short and you lose the benefit; too long and the result feels slow to appear.

🧭 Debouncing vs Throttling

Debouncing waits for the activity to stop and then runs once. A related technique called throttling runs the function at a steady rate while the activity continues, such as once every 200 milliseconds. Use debouncing when you only care about the final result, like a finished search query. Use throttling when you need regular updates during the action, like tracking scroll position.

They solve different problems

Debouncing answers “run after the user stops.” Throttling answers “run at most this often while the user keeps going.” We cover throttling in the next lesson.

⚠️ Common Mistakes to Avoid

Mistake Problem Solution
Forgetting clearTimeout Every call fires, so the function runs many times Cancel the old timer before starting a new one
Choosing the wrong delay Too short does nothing; too long feels sluggish Tune the delay to the use case, often 250 to 400 ms
Debouncing when throttling fits You miss updates needed during the action Use throttling for steady updates while events continue
Creating the debounced function inside the handler A new timer is made each time, so it never resets Call debounce once and reuse the returned function

🔧 Try It Yourself!

  1. Copy the debounce function and wrap a simple function that logs a message.
  2. Attach the debounced function to an input’s input event and watch it fire only after you stop typing.
  3. Lower the delay to 100 and raise it to 1000, then notice how the timing changes.
  4. Remove the clearTimeout line and confirm the function now fires on every keystroke.

🧩 What You’ve Learned

  • ✅ Debouncing delays a function until the triggering events stop for a set time
  • ✅ It suits rapid events like typing in a search box or resizing a window
  • setTimeout schedules the call and clearTimeout cancels the previous one
  • ✅ A closure keeps the timer id alive between calls
  • ✅ Debouncing runs after the activity stops, while throttling runs at a steady rate

🚀 What’s Next?

Now that you can wait for activity to stop, let’s learn how to run a function at a steady pace while it continues. Let’s continue to Throttling.

Share & Connect