Debounce and Throttle in JavaScript
What are Debounce and Throttle?
Debounce and Throttle are performance optimization techniques that control the frequency of function execution, especially when handling events that can be triggered very frequently (scroll, resize, input, etc.).
Debounce
Debounce delays function execution until a certain amount of time has passed since the last invocation.
When to use?
- Search with autocomplete (send request after user stops typing)
- Real-time form validation
- Draft saving (auto-save after pause in editing)
Implementation Example
function debounce(func, delay) {
let timeoutId;
return function(...args) {
// Clear previous timer
clearTimeout(timeoutId);
// Set new timer
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Usage
const searchInput = document.querySelector('#search');
const handleSearch = debounce((event) => {
console.log('Search:', event.target.value);
// API request
}, 500);
searchInput.addEventListener('input', handleSearch);
How does it work?
- On each function call, the previous timer is cleared
- A new timer is set
- The function executes only if
delaymilliseconds have passed since the last call
Result:
If user types "javascript", instead of 10 requests (one per letter), only one request will be sent 500ms after user finishes typing.
Throttle
Throttle ensures the function executes no more than once per specified time interval.
When to use?
- Handling scroll events (infinite scroll, lazy loading)
- Handling window resize
- Tracking mouse movement
- Submit button (protection from multiple clicks)
Implementation Example
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Usage
const handleScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
// Check if reached end of page
}, 1000);
window.addEventListener('scroll', handleScroll);
How does it work?
- On first call, function executes immediately
inThrottleflag is set totrue- All subsequent calls are ignored until
limitmilliseconds pass - After time expires, function can be called again
Result:
If user scrolls page rapidly, function will be called maximum once per second, instead of hundreds of times per second.
Comparing Debounce and Throttle
| Characteristic | Debounce | Throttle |
|---|---|---|
| Behavior | Executes after pause | Executes at intervals |
| First call | Delayed | Executes immediately |
| Frequent calls | All reset | Executes once per N ms |
| Use cases | Search, auto-save | Scroll, resize, tracking |
Visualization
Imagine an event occurs 10 times per second:
Without optimization:
█ █ █ █ █ █ █ █ █ █ (10 calls)
With Debounce (500ms):
░ ░ ░ ░ ░ ░ ░ ░ ░ █ (1 call after pause)
With Throttle (500ms):
█ ░ ░ ░ ░ █ ░ ░ ░ ░ (2 calls at intervals)
Advanced Implementation with leading and trailing
Debounce with options
function debounce(func, delay, { leading = false, trailing = true } = {}) {
let timeoutId;
return function(...args) {
const isInvokingLater = !timeoutId;
clearTimeout(timeoutId);
if (leading && isInvokingLater) {
func.apply(this, args);
}
timeoutId = setTimeout(() => {
if (trailing) {
func.apply(this, args);
}
timeoutId = null;
}, delay);
};
}
leading: true— call at the beginningtrailing: true— call at the end (default)
Throttle with options
function throttle(func, limit, { leading = true, trailing = true } = {}) {
let inThrottle;
let lastArgs;
return function(...args) {
if (!inThrottle) {
if (leading) {
func.apply(this, args);
}
inThrottle = true;
setTimeout(() => {
inThrottle = false;
if (trailing && lastArgs) {
func.apply(this, lastArgs);
lastArgs = null;
}
}, limit);
} else {
lastArgs = args;
}
};
}
Using Libraries
In production, ready-made solutions are often used:
Lodash
import { debounce, throttle } from 'lodash';
const debouncedSearch = debounce(searchFunction, 500);
const throttledScroll = throttle(scrollHandler, 1000);
Canceling execution
const debouncedFn = debounce(someFunction, 1000);
// Cancel pending call
debouncedFn.cancel();
React Example
import { useCallback, useRef, useEffect } from 'react';
function useDebounce(callback, delay) {
const timeoutRef = useRef(null);
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return useCallback((...args) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callback(...args);
}, delay);
}, [callback, delay]);
}
// Usage in component
function SearchComponent() {
const handleSearch = useDebounce((value) => {
console.log('Searching:', value);
}, 500);
return (
<input
type="text"
onChange={(e) => handleSearch(e.target.value)}
/>
);
}
Mistakes and Pitfalls
Forgetting about this context
// Wrong
function debounce(func, delay) {
let timeoutId;
return function() {
clearTimeout(timeoutId);
timeoutId = setTimeout(func, delay); // lose this
};
}
// Correct
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay); // ✅
};
}
Creating new debounced function on each render
// Wrong in React
function Component() {
// Each render creates new function
const handler = debounce(() => {}, 500);
return <input onChange={handler} />;
}
// Correct
function Component() {
// Function created once
const handler = useMemo(
() => debounce(() => {}, 500),
[]
);
return <input onChange={handler} />;
}
Conclusion
- Debounce — delays execution until pause in calls (search, auto-save)
- Throttle — limits execution frequency (scroll, resize)
- Both techniques are critical for application performance
- In production, use ready-made libraries (lodash, underscore)
- Don't forget about context (
this) and arguments in implementation - In React, use useCallback or useMemo to preserve functions between renders
In Interviews:
Often asked to implement debounce/throttle from scratch and explain differences. It's important to understand not only implementation but also practical use cases.