Debouncing and Throttling in JavaScript
January 4, 2026
Debouncing and Throttling in JavaScript
Overview
Debouncing and Throttling are techniques used to control how often a function is executed, especially useful for performance optimization in event handlers.
Key Differences
Throttling
- Limits the rate at which a function can be called
- Ensures a function is called at most once in a specified time period
- Useful for events that fire frequently like scrolling, resizing
- Example: If throttled to 1000ms, function will execute at most once per second
Debouncing
- Delays the execution of a function until after a specified wait time
- Resets the timer every time the function is called
- Only executes after the event has stopped firing for the specified delay
- Useful for search inputs, form submissions
- Example: If debounced to 1000ms, function will only execute after 1 second of no calls
Comparison Table
| Feature | Throttling | Debouncing | |---------|-----------|------------| | Timing | Guarantees execution at regular intervals | Waits for a pause in events | | Use Cases | Continuous events (scroll, resize) | Discrete events (search, form submit) | | Execution | May execute multiple times | Executes only once after delay | | Frequency | At most once per time period | After pause in events |
Debouncing
Trailing Debounce (Default)
Executes the function after the delay period when events stop firing.
function debounceTrailing(func, delay) {
let timeoutId;
return function (...args) {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
Example:
const searchInput = debounceTrailing((query) => {
console.log("Searching for:", query);
}, 300);
// User types "hello"
searchInput("h"); // Timer starts
searchInput("he"); // Timer resets
searchInput("hel"); // Timer resets
searchInput("hell"); // Timer resets
searchInput("hello"); // Timer resets
// After 300ms of no input, function executes with "hello"
Leading Debounce
Executes the function immediately on the first call, then ignores subsequent calls until the delay period passes.
function debounceLeading(func, delay) {
let timeoutId;
let called = false;
return function (...args) {
if (!called) {
func.apply(this, args); // Execute immediately
called = true;
}
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
called = false; // Reset after delay
}, delay);
};
}
Example:
const callMethod = debounceLeading((name) => {
console.log(name);
}, 300);
callMethod("1"); // Executes immediately
callMethod("2"); // Ignored
callMethod("3"); // Ignored
callMethod("4"); // Ignored
// After 300ms, can execute again
setTimeout(() => {
callMethod("5"); // Executes immediately
}, 400);
// Output: 1, 5
Throttling
Leading Throttle
Executes the function immediately on the first call, then ignores subsequent calls until the delay period passes.
function throttleLeading(func, delay) {
let isThrottled = false;
return function (...args) {
if (!isThrottled) {
func.apply(this, args); // Execute immediately
isThrottled = true;
setTimeout(() => {
isThrottled = false; // Reset after delay
}, delay);
}
};
}
Example:
const handleScroll = throttleLeading(() => {
console.log("Scrolling");
}, 1000);
// User scrolls rapidly
handleScroll(); // Executes immediately
handleScroll(); // Ignored (within 1000ms)
handleScroll(); // Ignored
// After 1000ms, can execute again
handleScroll(); // Executes immediately
Trailing Throttle
Does not run on the first call; it schedules one run after delay ms from the first call in a burst. While the timer is pending, later calls only update the stored arguments (they do not push the fire time later—that behavior is closer to debounce).
function throttleTrailing(func, delay) {
let timeoutId;
let lastArgs;
return function (...args) {
lastArgs = args;
if (!timeoutId) {
timeoutId = setTimeout(() => {
timeoutId = null;
func.apply(this, lastArgs); // Execute with latest arguments from the burst
}, delay);
}
};
}
Example:
const handleResize = throttleTrailing(() => {
console.log("Window resized");
}, 1000);
// User resizes window rapidly
handleResize(); // Timer starts; run is scheduled for ~1000ms from now
handleResize(); // Timer already pending — only updates last args
handleResize(); // Same
// ~1000ms after the *first* resize in this burst, runs once (with latest args)
Real-World Use Cases
1. Search Input (Debouncing)
const searchInput = document.getElementById('search');
const debouncedSearch = debounceTrailing((query) => {
// API call to search
fetch(`/api/search?q=${query}`)
.then(response => response.json())
.then(results => {
displayResults(results);
});
}, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
2. Window Resize (Throttling)
const handleResize = throttleLeading(() => {
// Recalculate layout
updateLayout();
}, 250);
window.addEventListener('resize', handleResize);
3. Scroll Events (Throttling)
const handleScroll = throttleTrailing(() => {
// Update scroll position indicator
updateScrollIndicator();
}, 100);
window.addEventListener('scroll', handleScroll);
4. Button Clicks (avoid double submit)
Trailing debounce on click is usually a poor fit: every click resets the timer, so the handler may never run if the user clicks again within the delay, or the submit feels laggy.
Prefer leading debounce (first click runs, rapid repeats ignored) or disable the button until the request finishes:
const submitButton = document.getElementById('submit');
const debouncedSubmit = debounceLeading(() => {
submitForm();
}, 1000);
submitButton.addEventListener('click', debouncedSubmit);
Complete Example
// Debounce implementation
function debounceTrailing(func, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Throttle implementation
function throttleLeading(func, delay) {
let isThrottled = false;
return function (...args) {
if (!isThrottled) {
func.apply(this, args);
isThrottled = true;
setTimeout(() => {
isThrottled = false;
}, delay);
}
};
}
// Usage
const greet = (name) => {
console.log(`Hello, ${name}!`);
};
// Debounce example
const debouncedGreet = debounceTrailing(greet, 300);
debouncedGreet("Alice"); // Timer starts
debouncedGreet("Bob"); // Timer resets
debouncedGreet("Charlie"); // Timer resets
// After 300ms: "Hello, Charlie!" (only last call executes)
// Throttle example
const throttledGreet = throttleLeading(greet, 300);
throttledGreet("Alice"); // Executes immediately
throttledGreet("Bob"); // Ignored
throttledGreet("Charlie"); // Ignored
// After 300ms, can execute again
setTimeout(() => {
throttledGreet("David"); // Executes immediately
}, 400);
Best Practices
-
Use debouncing for:
- Search input fields
- Form submissions
- API calls triggered by user input
- Window resize (if you only care about final size)
-
Use throttling for:
- Scroll events
- Mouse move events
- Window resize (if you need updates during resize)
- Continuous user interactions
-
Choose appropriate delays:
- Search: 300-500ms
- Scroll/Resize: 100-250ms
- Double-submit: short leading debounce (e.g. 300–500ms), or disable the control until the async work completes (often better than a long timer)
-
Consider context binding:
- Use
func.apply(this, args)to preservethiscontext - Important when debouncing/throttling methods
- Use
Key Takeaways
- Debouncing delays execution until events stop firing
- Throttling limits execution frequency to at most once per time period
- Trailing executes after delay, Leading executes immediately
- Use debouncing for search, form submissions, API calls
- Use throttling for scroll, resize, continuous events
- Always preserve context with
apply()or arrow functions - Choose appropriate delays based on use case