JavaScript Snippet
JavaScript Debounce & Throttle
The two patterns that make expensive events cheap — with interactive live demos.
Last updated: October 2026
Quick answer: Debounce delays a function until events stop firing — perfect for search inputs and form validation. Throttle runs a function at a fixed interval while events fire — perfect for scroll and resize handlers. Both dramatically reduce how often expensive code runs.
🎬 Interactive Live Demo
Type in the input boxes and watch the difference between debounce and throttle in real time.
💡 Type fast in both boxes — debounce runs once, throttle runs repeatedly at 500ms intervals.
What is Debounce?
Debounce delays a function call until after a specified delay has passed with no further calls. If new events keep firing, the timer resets.
Analogy: an elevator door that waits for the last person to step in before closing. Every new passenger resets the timer.
Use for:
- Search input (only fetch after user stops typing)
- Form validation on typing
- Autosave while editing
- Window resize handlers
What is Throttle?
Throttle limits a function to run at most once every N milliseconds, no matter how many times the event fires. Continuous events produce steady, predictable calls.
Analogy: a machine gun with a maximum rate of fire. You can hold the trigger, but bullets only come out at the defined rate.
Use for:
- Scroll progress indicators
- Mouse move tracking
- Infinite scroll triggers
- Rate-limiting API calls
The Debounce Function
Here is a clean, dependency-free implementation:
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
// Usage
const handleSearch = debounce((query) => {
console.log("Searching for:", query);
}, 500);
document.querySelector("#search").addEventListener("input", (e) => {
handleSearch(e.target.value);
});
Every keystroke clears the previous timer and starts a new one. Only after 500ms of silence does the function actually run.
The Throttle Function
Here's a version that fires on both leading and trailing edges:
function throttle(fn, delay) {
let lastCall = 0;
let timeoutId;
return function(...args) {
const now = Date.now();
const remaining = delay - (now - lastCall);
if (remaining <= 0) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
lastCall = now;
fn.apply(this, args);
} else if (!timeoutId) {
timeoutId = setTimeout(() => {
lastCall = Date.now();
timeoutId = null;
fn.apply(this, args);
}, remaining);
}
};
}
// Usage
const handleScroll = throttle(() => {
console.log("Scroll position:", window.scrollY);
}, 200);
window.addEventListener("scroll", handleScroll);
This ensures the function runs immediately on the first event, then at most every 200ms, and always fires one last time after events stop.
Real-World Example: Search Autocomplete
import { useState, useEffect } from "react";
function SearchBox() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) return;
const controller = new AbortController();
const timeoutId = setTimeout(() => {
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(res => res.json())
.then(setResults)
.catch(err => {
if (err.name !== "AbortError") console.error(err);
});
}, 500);
return () => {
clearTimeout(timeoutId);
controller.abort();
};
}, [query]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
<ul>
{results.map(r => <li key={r.id}>{r.title}</li>)}
</ul>
</div>
);
}
This combines debounce (via setTimeout) with AbortController to cancel in-flight requests. The result: one clean API call after the user stops typing.
👉 Read our AbortController Guide.
🎯 Debounce vs Throttle — Quick Reference
| Aspect | Debounce | Throttle |
|---|---|---|
| When it runs | After events stop | At fixed intervals during events |
| Total calls | 1 (after burst) | Multiple (steady rate) |
| Best for | Search, validation, autosave | Scroll, resize, mouse tracking |
| Feels like | "Wait until done" | "Update regularly" |
⚠️ Common Mistakes
- Using debounce when throttle is needed. A scroll handler debounced fires only after scrolling stops — you want consistent updates during scroll. Use throttle.
- Forgetting to bind
this. The.apply(this, args)inside the wrapper preserves the calling context. Omitting it breaks object methods. - Creating a new debounced function on every render. In React, wrap the debounced function in
useReforuseMemoso it survives re-renders. - Not cancelling on cleanup. In React effects, always return a cleanup that calls
clearTimeoutto prevent stale updates. - Using debounce/throttle where it does nothing. For a one-shot user click, they add latency without benefit.
- Assuming debounce cancels in-flight requests. It doesn't — you need
AbortControllerfor that.
❓ Frequently Asked Questions
What is the difference between debounce and throttle?
Debounce waits until events stop firing, then runs once. Throttle runs at a fixed interval while events fire. Use debounce for search inputs, throttle for scroll events.
When should I use debounce?
Use debounce when the user's final action matters more than intermediate ones. Examples: search input, form validation on typing, autosave, window resize handled at the end.
When should I use throttle?
Use throttle when you need consistent updates at a fixed rate. Examples: scroll progress indicators, mouse tracking, infinite scroll triggers, window resize during drag.
Does debounce or throttle improve performance?
Both improve performance by reducing how often expensive functions run. Debounce reduces calls when events are bursty (typing). Throttle reduces calls when events are continuous (scroll).
Can I use Lodash debounce and throttle instead?
Yes. Lodash provides battle-tested debounce and throttle with options like leading, trailing, and maxWait. If you already use Lodash, prefer those implementations. The custom versions here are dependency-free.