What you'll be able to do

By the end of this page you should be able to explain debounce in plain words, write a small debounce helper, and wire it to an input so you do not fire work on every keystroke.

Previous post: regexp basics for forms.

Who this is for

  • People whose search box hits the API on every letter
  • Beginners who heard "debounce" in interviews and wanted a calm build
  • Anyone whose resize or scroll handler feels noisy

You can skip this if your UI library already debounces and you understand the idea. Building it once makes the word stick.

The problem: too many events

input.addEventListener("input", function () {
  // runs on every key — expensive if this fetches
  search(input.value);
});

Scenario A — type "hello". Five fetches, four of them wasted.

Scenario B — window resize. Hundreds of layout reads while the user drags.

Debounce means: wait until events stop for N milliseconds, then run once.

A small debounce helper

function debounce(fn, waitMs) {
  let timerId;
  return function (...args) {
    clearTimeout(timerId);
    timerId = setTimeout(function () {
      fn.apply(this, args);
    }, waitMs);
  };
}

Scenario A — search. const debouncedSearch = debounce(search, 300); then call debouncedSearch(value) on input.

Scenario B — save draft. Debounce localStorage writes so you are not writing on every key.

Each new event resets the timer; only the quiet gap lets fn run.

Contrast with "run immediately"

Debounce is not "slow the function down forever." It is "group a burst into one call after calm."

Scenario A — user pauses after a word — one search.

Scenario B — user keeps typing — still waiting; that is intended.

A tiny practice file

<!DOCTYPE html>
<html>
  <body>
    <input id="q" placeholder="Type to search..." />
    <pre id="log"></pre>
    <script>
      function debounce(fn, waitMs) {
        let timerId;
        return function (...args) {
          clearTimeout(timerId);
          timerId = setTimeout(function () {
            fn.apply(this, args);
          }, waitMs);
        };
      }
      const log = document.getElementById("log");
      const runSearch = debounce(function (text) {
        log.textContent += "search: " + text + "\n";
      }, 400);
      document.getElementById("q").addEventListener("input", function (e) {
        runSearch(e.target.value);
      });
    </script>
  </body>
</html>

Type quickly — you should see one log line after you pause.

Mistakes I see a lot

1. Creating a new debounced function inside the listener on every event — that resets nothing useful; create it once outside.

2. Debounce wait of 0 or 5ms thinking it is magic — pick something like 200–400ms for search UX and tune.

3. Debouncing clicks that must feel instant — submit buttons usually want immediate response (or throttle/disable), not a long pause.

4. Forgetting clearTimeout so old timers still fire.

What to try before the next post

  1. Run the tiny HTML demo and change 400 to 150 / 800 to feel the UX.
  2. Debounce a fake fetch with console.log.
  3. Read the next post on throttle and say out loud when you would pick each.

Next in this series: throttle from zero — run at most once per time window while events keep firing.

Try this next outside the series

Deeper JavaScript pays off when you connect it to routes, imports, and performance-sensitive app code.