⏱️
Intermediate
9 min readDebouncing & Throttling
Controlling function execution frequency for better performance
Understanding Debounce and Throttle
Debouncing and throttling are techniques to control how often a function executes, particularly useful for expensive operations triggered by frequent events like scrolling, resizing, or typing.
Debounce
Debounce delays function execution until after a specified time has passed since the last invocation. Perfect for search inputs and auto-save features.
// Simple debounce implementation
function debounce(func, delay) {
let timeoutId;
return function(...args) {
// Clear previous timeout
clearTimeout(timeoutId);
// Set new timeout
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// ❌ Bad: Search API called on every keystroke
searchInput.addEventListener('input', (e) => {
searchAPI(e.target.value); // Called hundreds of times
});
// ✅ Good: Search API called only after user stops typing
const debouncedSearch = debounce((query) => {
searchAPI(query);
}, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// Advanced debounce with leading edge option
function debounceAdvanced(func, delay, options = {}) {
let timeoutId;
let lastCallTime = 0;
return function(...args) {
const now = Date.now();
const isLeading = options.leading && (now - lastCallTime > delay);
clearTimeout(timeoutId);
if (isLeading) {
func.apply(this, args);
lastCallTime = now;
}
timeoutId = setTimeout(() => {
if (!options.leading || !isLeading) {
func.apply(this, args);
}
lastCallTime = Date.now();
}, delay);
};
}
Throttle
Throttle ensures a function executes at most once per specified time period. Perfect for scroll events and window resize handlers.
// Simple throttle implementation
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// ❌ Bad: Scroll handler called hundreds of times per second
window.addEventListener('scroll', () => {
updateScrollPosition(); // Called excessively
});
// ✅ Good: Scroll handler called at most every 100ms
const throttledScroll = throttle(() => {
updateScrollPosition();
}, 100);
window.addEventListener('scroll', throttledScroll);
// Advanced throttle with trailing edge
function throttleAdvanced(func, limit, options = {}) {
let timeout;
let previous = 0;
return function(...args) {
const now = Date.now();
const remaining = limit - (now - previous);
if (remaining <= 0 || remaining > limit) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(this, args);
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(() => {
previous = options.leading === false ? 0 : Date.now();
timeout = null;
func.apply(this, args);
}, remaining);
}
};
}
Debounce vs Throttle Comparison
// Visualizing the difference
const events = [0, 50, 100, 150, 200, 250, 300, 350, 400];
// Without debounce/throttle: Function called 9 times
// Output: [0, 50, 100, 150, 200, 250, 300, 350, 400]
// With debounce (100ms): Function called 1 time
// Output: [400] (only after user stops)
// With throttle (100ms): Function called 5 times
// Output: [0, 100, 200, 300, 400]
Real-World Use Cases
// 1. Auto-save (debounce)
const autoSave = debounce((content) => {
saveToServer(content);
}, 1000);
editor.addEventListener('input', (e) => {
autoSave(e.target.value);
});
// 2. Infinite scroll (throttle)
const loadMore = throttle(() => {
if (isNearBottom()) {
fetchMoreItems();
}
}, 200);
window.addEventListener('scroll', loadMore);
// 3. Window resize (debounce)
const handleResize = debounce(() => {
recalculateLayout();
}, 250);
window.addEventListener('resize', handleResize);
// 4. Button click prevention (throttle)
const submitForm = throttle((formData) => {
sendToAPI(formData);
}, 2000, { trailing: false });
button.addEventListener('click', () => {
submitForm(getFormData());
});
// 5. Search with autocomplete (debounce)
const autocomplete = debounce(async (query) => {
if (query.length < 3) return;
const results = await fetchSuggestions(query);
displaySuggestions(results);
}, 300);
searchBox.addEventListener('input', (e) => {
autocomplete(e.target.value);
});
React Implementation
// Custom React hooks for debounce and throttle
import { useEffect, useCallback, useRef } 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]);
}
function useThrottle(callback, limit) {
const inThrottleRef = useRef(false);
return useCallback((...args) => {
if (!inThrottleRef.current) {
callback(...args);
inThrottleRef.current = true;
setTimeout(() => {
inThrottleRef.current = false;
}, limit);
}
}, [callback, limit]);
}
// Usage in component
function SearchComponent() {
const handleSearch = useDebounce((query) => {
fetchResults(query);
}, 300);
return (
handleSearch(e.target.value)}
placeholder="Search..."
/>
);
}
When to Use Each
- Debounce: Search boxes, auto-save, form validation, API calls after typing
- Throttle: Scroll handlers, window resize, mousemove tracking, animation frame limiting
- Use debounce when you want the final value after events stop
- Use throttle when you want consistent updates at a fixed rate
- Debounce delay: 200-500ms for typing, 250-500ms for resize
- Throttle limit: 100-200ms for scroll, 16ms for animations (60fps)
- Always clean up event listeners to prevent memory leaks
- Consider using
requestAnimationFramefor scroll-based animations