Debouncing in JavaScript: A Complete Tutorial

Learn how to debounce functions in JavaScript to control how often expensive operations run. Master the pattern behind search inputs, resize handlers, and auto-save.

7 min read

Debouncing is a technique that delays a function call until a burst of activity stops. If a function is called repeatedly in quick succession, only the last call runs after a quiet period.

The classic use case is a search input. As the user types, you do not want to fire an API request on every keystroke. You want to wait until they stop typing, then send one request.

The Problem Without Debouncing

javascriptjavascript
const searchInput = document.querySelector("#search");
 
searchInput.addEventListener("input", (e) => {
  fetch(`/api/search?q=${e.target.value}`)
    .then(res => res.json())
    .then(data => renderResults(data));
});

Typing "hello" fires 5 API requests: one each for "h", "he", "hel", "hell", and "hello". The first four are wasted. They return results the user no longer cares about. They consume server resources and bandwidth for nothing.

The Basic Debounce Function

A debounce function wraps your original function and only calls it after a specified quiet period:

javascriptjavascript
function debounce(fn, delay) {
  let timerId = null;
 
  return function (...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

Every call clears the previous timer and starts a new one. Only when calls stop for delay milliseconds does the function actually execute:

javascriptjavascript
const searchInput = document.querySelector("#search");
 
const debouncedSearch = debounce((query) => {
  console.log(`Searching for: ${query}`);
}, 300);
 
searchInput.addEventListener("input", (e) => {
  debouncedSearch(e.target.value);
});

Now typing "hello" produces one log line, 300ms after the last keystroke:

texttext
Searching for: hello
Debounce: calls reset the timer, only last call executes

Each keystroke resets the countdown. Only when the user pauses for 300ms does the search actually happen.

Leading Edge Debounce

Sometimes you want the function to fire immediately on the first call, then ignore subsequent calls until the quiet period passes. This is the leading edge variant:

javascriptjavascript
function debounce(fn, delay, leading = false) {
  let timerId = null;
 
  return function (...args) {
    const callNow = leading && timerId === null;
 
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      timerId = null;
      if (!leading) fn.apply(this, args);
    }, delay);
 
    if (callNow) fn.apply(this, args);
  };
}

Leading edge is useful for buttons. If the user clicks "Save" rapidly, you want the first click to save immediately, not wait for them to stop clicking:

javascriptjavascript
const saveButton = document.querySelector("#save-btn");
 
const debouncedSave = debounce(() => {
  console.log("Saving...");
  // Actual save logic here
}, 1000, true);
 
saveButton.addEventListener("click", debouncedSave);

First click: saves immediately. Rapid subsequent clicks: ignored. After 1 second of no clicks: ready for the next save.

Debounce with Cancel

A well-built debounce function should let you cancel a pending call. This matters when the user navigates away or unmounts a component:

javascriptjavascript
function debounce(fn, delay) {
  let timerId = null;
 
  function debounced(...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      fn.apply(this, args);
      timerId = null;
    }, delay);
  }
 
  debounced.cancel = () => {
    clearTimeout(timerId);
    timerId = null;
  };
 
  debounced.flush = () => {
    if (timerId !== null) {
      clearTimeout(timerId);
      fn();
      timerId = null;
    }
  };
 
  return debounced;
}

Usage with cleanup:

javascriptjavascript
const debouncedAutoSave = debounce(saveToServer, 2000);
 
// User edits a document
editor.addEventListener("input", debouncedAutoSave);
 
// User navigates away -- flush pending save immediately
window.addEventListener("beforeunload", () => {
  debouncedAutoSave.flush();
});

cancel discards the pending call. flush executes it immediately.

Debounce for Auto-Save

Auto-save is a perfect use case. Save after the user pauses, not on every keystroke:

javascriptjavascript
function createAutoSave(storageKey, delay = 1500) {
  const save = debounce((content) => {
    localStorage.setItem(storageKey, content);
    console.log("Auto-saved at", new Date().toLocaleTimeString());
  }, delay);
 
  return {
    onChange(content) {
      save(content);
    },
    flush() {
      save.flush();
    }
  };
}
 
const autoSave = createAutoSave("draft-post");
 
document.querySelector("#editor").addEventListener("input", (e) => {
  autoSave.onChange(e.target.value);
});
 
// Save immediately before closing
window.addEventListener("beforeunload", () => {
  autoSave.flush();
});

Debounce for Window Resize

Resize events fire rapidly. Debounce expensive layout calculations:

javascriptjavascript
const handleResize = debounce(() => {
  console.log(`Window size: ${window.innerWidth}x${window.innerHeight}`);
  recalculateLayout();
}, 200);
 
window.addEventListener("resize", handleResize);

Without debounce, resizing a window from 1200px to 800px might fire 50 events. With debounce, only the final size matters.

When NOT to Use Debounce

Debounce is wrong when you need continuous updates during activity. For scroll position tracking, animation frame callbacks, or real-time mouse following, use throttling instead.

Use debounce forUse throttle for
Search-as-you-typeScroll position tracking
Auto-saveResize animations
Form validation after typingMouse move effects
Final window size after resizeGame loop updates

If you need to limit total calls over a time window regardless of activity patterns, use a rate limiter.

Common Mistakes

Debounce delay too short. A 50ms debounce on a search input is pointless. The user cannot type fast enough to benefit. Use at least 200-300ms.

Forgetting to clean up. If you debounce inside a component that unmounts, cancel the timer. Otherwise the callback may fire on unmounted DOM.

Using debounce for visual updates. If the user needs smooth visual feedback (scroll position, drag position, cursor follower), use requestAnimationFrame or throttle. Debounce will feel laggy because it waits for activity to stop.

Rune AI

Rune AI

Key Insights

  • Debouncing delays function execution until calls stop for a specified wait time.
  • Every new call resets the timer. Only the last call in a burst executes.
  • The trailing edge version (most common) fires after activity stops.
  • The leading edge version fires immediately on the first call, then ignores subsequent calls.
  • Always provide a cancel method to clean up pending timers.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between debouncing and throttling?

Debouncing waits until activity stops, then executes once. Throttling executes at a fixed rate during activity. Use debounce for search inputs (wait until typing stops). Use throttle for scroll handlers (fire regularly during scrolling).

What debounce delay should I use?

Search inputs: 250-400ms. Auto-save: 1000-2000ms. Resize handlers: 150-300ms. Start with 300ms and adjust based on feel. Too fast defeats the purpose. Too slow feels unresponsive.

Can I cancel a pending debounced call?

Yes. A properly implemented debounce function returns a cancel method or the debounced function itself has a .cancel() property. Call it to clear the pending timer.

Conclusion

Debouncing is the go-to pattern for delaying execution until a burst of activity stops. Use it for search inputs, auto-save, resize handlers, and any case where you want to wait for the user to finish before doing expensive work.Debouncing answers one question: "has the user finished?" Wrap your function in debounce(fn, delay). Every call resets the timer. Only when calls stop for delay milliseconds does fn run. The trailing edge version (default) is for search, auto-save, and resize. The leading edge version is for button clicks where you want the first action to be immediate. Always provide a cancel method for cleanup.