What are Debounce and Throttle?
When you build web apps, some events happen very quickly, like typing, scrolling, or resizing the browser window.
If your function runs every time one of these events happens, it can slow down your app.
That’s where debounce and throttle help.
They are simple ways to control how often a function runs, especially for fast, repeated events.
To put it simply:
- Debounce: Wait until the user stops doing something, then run the function. Like waiting for someone to finish talking before you reply.
- Throttle: Limit the function to run at regular time gaps, no matter how often the event happens. Like saying "I’ll respond only once every 2 seconds," even if the event keeps firing.
These techniques improve performance and make your app feel smoother.
Why You Need Debounce and Throttle
Imagine calling a search API every time a user types a letter.
<input onkeyup="searchSuggestions()" />
Typing "apple" fires 5+ events and calls the function 5+ times. That’s unnecessary and wasteful.
Instead, you can:
- Debounce: Wait until the user stops typing, then call the API.
- Throttle: Limit API calls to maybe once every 300ms, even while typing.
Both approaches help reduce unnecessary work and make your app faster and smoother.
Debounce
Debounce delays function execution until after a specified time has passed since the last time it was called. If the function is called again before the delay ends, the timer resets.
You can think of it like this:
- You’re typing a search query. Debounce says: "I’ll wait until you stop typing for 300ms before searching."
- If you keep typing, it keeps resetting the timer.
How Debounce Works
- You give it a function (like a search)
- It delays running it until after a pause
- If the event happens again (like another keypress), it resets the timer
Example of Debounce
function debounce(func, delay) {
let timeoutId;
return function (...args) {
// Clear the previous timer
clearTimeout(timeoutId);
// Set a new timer
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Usage example
const debouncedSearch = debounce(function (event) {
console.log(`Searching for: "${event.target.value}"`);
// API call would go here
}, 300); // Wait 300ms after user stops typing
document.getElementById("search").addEventListener("input", debouncedSearch);
- As the user types,
debouncedSearch
keeps waiting. - Only after 300ms of no typing, it actually runs.
- Prevents unnecessary API calls on every keystroke.
When to Use Debounce
- Search input: Wait for user to stop typing
- Form validation: Validate after user finishes editing
- API calls: Prevent excessive requests
- Window resize: Recalculate layouts after resizing ends
- Button clicks: Prevent double-submissions
Throttle
Throttle limits function execution to at most once per specified time interval. Even if an event fires 100 times, the function will only execute once during each interval.
You can think of it like this:
- You’re scrolling a page. Throttle says: "I’ll run this function only once every 100ms, no matter how often you scroll."
How Throttle Works
- It runs the function immediately the first time
- Then waits for a fixed time (limit) before allowing it to run again
- Ignores any event fires during that wait
Example of Throttle
function throttle(func, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Usage example
const throttledScrollHandler = throttle(function () {
console.log(`Scroll position: ${window.scrollY}`);
}, 100); // Execute at most once every 100ms
window.addEventListener("scroll", throttledScrollHandler);
- When you scroll, it logs the position, but only once every 100ms.
- It skips any event fires in between.
- It's great for scroll, resize, and mousemove events.
When to Use Throttle
- Scroll events: Update UI elements during scrolling
- Mouse movement: Track cursor position
- Animation frames: Limit animation updates
- Progress tracking: Update progress indicators
- Real-time data: Limit data processing frequency
Leading vs Trailing Throttle
There are two types of throttle behavior:
Leading Throttle:
- Executes the function immediately when first called
- Then waits for the specified interval before allowing another execution
Trailing Throttle:
- Waits for the specified interval before the first execution
- Executes the function at the end of each interval
function throttleTrailing(func, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
inThrottle = true;
setTimeout(() => {
func.apply(this, args);
inThrottle = false;
}, limit);
}
};
}
When to use each:
- Leading: When you want immediate response (like button clicks).
- Trailing: When you want to capture the final state (like scroll position).
Common Mistakes and How to Avoid Them
Debounce Mistakes
1. Delay Too Long = Laggy Feel
// BAD: 1 second delay feels unresponsive
const debouncedSearch = debounce(searchFunction, 1000);
// GOOD: 300ms is usually the sweet spot
const debouncedSearch = debounce(searchFunction, 300);
2. Never Executing on Continuous Events
If a user types continuously without pausing, debounce might never execute:
// Problem: If user types for 10 seconds straight, no search happens
const debouncedSearch = debounce(searchFunction, 500);
// Solution: Combine with maxWait
function debounceWithMaxWait(func, delay, maxWait) {
let timeoutId;
let maxTimeoutId;
return function (...args) {
clearTimeout(timeoutId);
if (!maxTimeoutId) {
maxTimeoutId = setTimeout(() => {
func.apply(this, args);
clearTimeout(timeoutId);
maxTimeoutId = null;
}, maxWait);
}
timeoutId = setTimeout(() => {
func.apply(this, args);
clearTimeout(maxTimeoutId);
maxTimeoutId = null;
}, delay);
};
}
3. Lost Context in React/Vue Components
// BAD: 'this' context might be lost
const debouncedHandler = debounce(this.handleSearch, 300);
// GOOD: Use arrow functions or bind
const debouncedHandler = debounce((event) => {
this.handleSearch(event);
}, 300);
Throttle Mistakes
1. Missing the Final Event
Throttle might miss the final state, for example, the last scroll position when the user stops scrolling.
// Problem: Final scroll position might not be captured
const throttledScroll = throttle(updateScrollPosition, 100);
// Solution: Combine throttle with debounce for final execution
function throttleWithFinal(func, limit) {
let inThrottle;
let lastArgs;
let finalTimeoutId;
return function (...args) {
lastArgs = args;
// Clear any pending final execution
clearTimeout(finalTimeoutId);
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
// Execute with final arguments after throttle period
finalTimeoutId = setTimeout(() => {
func.apply(this, lastArgs);
}, 50);
}, limit);
}
};
}
2. Inconsistent Timing
// BAD: Throttle interval starts after function execution
function throttleBad(func, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
func.apply(this, args); // Function runs first
inThrottle = true;
setTimeout(() => (inThrottle = false), limit); // Then timer starts
}
};
}
// GOOD: Use more precise timing
function throttleGood(func, limit) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= limit) {
lastCall = now;
func.apply(this, args);
}
};
}
👉 Next tutorial: JavaScript Generators and Iterators