BACK

Oct 5, 2025

0 words
0 m

Understanding Debouncing and Throttling

If you’ve ever built a search bar, an infinite scroll, or a resize listener in JavaScript, you’ve probably run into performance problems. Too many function calls, apps lagging, API endpoints being spammed.

That’s where debouncing and throttling come in. These are two simple patterns that make your apps faster, smoother, and more user-friendly.

In this guide, we’ll break down what they are, how to code them from scratch, when to use each, and the most common mistakes developers make. By the end, you’ll have reusable snippets you can drop into your projects.

Why You Need Them

Imagine a search bar that makes an API request every time you type a letter. Typing "hello" triggers 5 API calls. On a slow network, that feels broken.

Or a resize event handler that runs on every pixel you resize the window. That can fire hundreds of times per second.

Both cases waste CPU cycles, clog up the network, and frustrate users.

Instead, you want to control how often a function runs. That’s exactly what debouncing and throttling do.

Debouncing: Wait Until the User Pauses

Debouncing means: wait until the action stops, then run the function once.

It’s like saying: “Don’t call the function until the user has stopped typing for 300ms.”

Example: Debounced Search Bar

<input type="text" id="search" placeholder="Search..." />

<ul id="results"></ul>

<script>
  function debounce(fn, delay) {
    let timeoutId;
    return function(...args) {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => fn.apply(this, args), delay);
    };
  }

  async function fetchResults(query) {
    console.log("Fetching for:", query);
    // Fake API
    const results = ["apple", "banana", "cherry"].filter(item =>
      item.includes(query.toLowerCase())
    );
    document.getElementById("results").innerHTML =
      results.map(r => `<li>${r}</li>`).join("");
  }

  const input = document.getElementById("search");
  input.addEventListener("input", debounce(e => {
    fetchResults(e.target.value);
  }, 400));
</script>

Now, typing “hello” triggers just one request after you stop typing.

Throttling: Limit How Often It Runs

Throttling means: run the function at most once every X milliseconds.

It’s like saying: “You can fire this scroll handler only once every 200ms, no matter how often the event happens.”

Example: Throttled Scroll Logger

<script>
  function throttle(fn, limit) {
    let inThrottle;
    return function(...args) {
      if (!inThrottle) {
        fn.apply(this, args);
        inThrottle = true;
        setTimeout(() => inThrottle = false, limit);
      }
    };
  }

  function handleScroll() {
    console.log("Scroll event at", window.scrollY);
  }

  window.addEventListener("scroll", throttle(handleScroll, 200));
</script>

Now scrolling fires logs every ~200ms instead of hundreds per second.

Debounce vs Throttle: Which One?

  • Use debounce when you only care about the final action.

    • Search bars.

    • Resizing windows (adjust layout after done resizing).

    • Saving draft after user stops typing.

  • Use throttle when you care about regular updates.

    • Scroll events (infinite scroll).

    • Resizing windows for live feedback.

    • Button spam prevention.

Advanced Options

Both patterns can be extended with extra control:

Debounce with Immediate Option

Sometimes you want the function to fire immediately on the first event, then wait for silence.

function debounce(fn, delay, immediate = false) {
  let timeoutId;
  return function(...args) {
    const callNow = immediate && !timeoutId;
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      timeoutId = null;
      if (!immediate) fn.apply(this, args);
    }, delay);
    if (callNow) fn.apply(this, args);
  };
}
  • debounce(fn, 500, true) will run instantly on the first keystroke, then wait until user stops.

Throttle with Leading and Trailing Calls

Sometimes you want both: run at the start, and also one last call at the end.

function throttle(fn, limit) {
  let lastCall = 0;
  let timeoutId;
  return function(...args) {
    const now = Date.now();
    if (now - lastCall >= limit) {
      fn.apply(this, args);
      lastCall = now;
    } else {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => {
        fn.apply(this, args);
        lastCall = Date.now();
      }, limit - (now - lastCall));
    }
  };
}

Using Libraries

You don’t have to reinvent the wheel every time. Popular libraries like Lodash and Underscore ship with tested, optimized versions.

import { debounce, throttle } from "lodash";

window.addEventListener("resize", debounce(() => {
  console.log("resize done");
}, 300));

window.addEventListener("scroll", throttle(() => {
  console.log("scrolling...");
}, 200));

In React, there are also hook-based solutions:

  • useDebounce (community hook)

  • useThrottle

Debugging and Testing Performance

To see the difference:

  1. Open DevTools → Performance tab.

  2. Add a raw scroll handler without throttle. Scroll quickly. You’ll see a storm of events.

  3. Add a throttled handler. Run again. You’ll see fewer, evenly spaced calls.

This is how you can prove the impact of these patterns.

Common Mistakes to Avoid

  • Forgetting to clear timers (leads to memory leaks).

  • Debouncing with too short a delay (still too many calls).

  • Throttling with too long a delay (app feels laggy).

  • Using debounce where throttle is better, and vice versa.

  • Not considering accessibility (e.g. debounce delays can confuse screen reader users if feedback is too slow).

Real-World Use Cases

  • Autocomplete search → Debounce API requests.

  • Infinite scroll → Throttle fetching next page.

  • Resizing canvas → Debounce expensive redraw.

  • Button double-click → Throttle or lock for 500ms.

  • Chat apps → Debounce “user is typing…” indicators.

Summary Checklist

  • ✅ Use debounce when you want the function to run after the user stops.

  • ✅ Use throttle when you want it to run at regular intervals.

  • ✅ Add options for leading and trailing execution.

  • ✅ Use libraries like Lodash if you want reliability.

  • ✅ Test in DevTools to measure actual performance.

  • ✅ Avoid common mistakes like un-cleared timers.

Final Thought:
Debouncing and throttling may sound like small optimizations, but they directly affect how “fast” your app feels. They save bandwidth, protect servers, and keep users happy. Master them, and you’ll notice your apps instantly feel smoother.

Taseen Tanvir

1:12:28 UTC

Create a free website with Framer, the website builder loved by startups, designers and agencies.