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
resizeorscrollhandler 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
- Run the tiny HTML demo and change 400 to 150 / 800 to feel the UX.
- Debounce a fake
fetchwithconsole.log. - 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.
- Next.js route handlers — see modules and request logic in a production-shaped route file
- HTML to JSX Converter — use the tool when deeper syntax work starts touching React files