How to implement debouncing and throttling in vanilla JavaScript
Learn to optimize event handlers by implementing debouncing and throttling functions from scratch using vanilla JavaScript without external libraries.
Implementing debouncing and throttling reduces unnecessary function executions during rapid events like scrolling or typing. These techniques prevent browser lag and save computational resources by limiting how often a callback runs. You will create reusable utility functions that work in any vanilla JavaScript environment without external dependencies.
Prerequisites
- A modern web browser with JavaScript enabled.
- A code editor like VS Code or Sublime Text.
- A basic understanding of JavaScript closures and timers.
- A simple HTML file to test the functions.
Step 1: Create a debounce function
Create a function that waits for a specified period of inactivity before executing the callback. The logic cancels any pending execution if a new event occurs before the timeout expires. This ensures the function only runs once after the user stops interacting.
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
You will see the closure preserves the `func` and `delay` variables for the returned inner function. The `clearTimeout` call removes the previous timer, resetting the clock on every new event. This pattern prevents the function from firing until the user stops triggering events for the set duration.
Step 2: Create a throttle function
Implement a function that restricts execution to a fixed frequency regardless of how often events trigger. Use a flag to track whether the function has executed recently and block execution until the delay period passes. This ensures the callback runs at least once every N milliseconds.
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
The `inThrottle` flag acts as a gatekeeper. When true, the function ignores the call and waits for the timeout to clear. Once the timer finishes, the flag resets to false, allowing the next execution. This approach is essential for scroll or resize events where you only need the latest state.
Step 3: Test debounce with an input field
Create an HTML file with an input box and a button to log messages to the console. Attach the debounced function to the input's `input` event to simulate a search query. You will observe that the log message only appears after you stop typing for the specified delay.
<!DOCTYPE html>
<html>
<head>
<title>Debounce Test</title>
<style>
body { font-family: sans-serif; padding: 20px; }
input { padding: 8px; width: 200px; }
button { padding: 8px; margin-top: 10px; }
</style>
<script>
const log = (msg) => console.log(msg);
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
const handleInput = debounce((value) => {
log(`Searching for: ${value}`);
}, 500);
const input = document.querySelector('input');
input.addEventListener('input', (e) => handleInput(e.target.value));
</script>
</head>
<body>
<input type="text" placeholder="Type to search..."/>
</body>
</html>
Run this file in a browser and open the developer console. Type rapidly in the input field. You will see the console log updates only after you pause typing for 500 milliseconds. This confirms the debounce logic cancels previous timeouts correctly.
Step 4: Test throttle with a scroll event
Add a scroll event listener that logs the scroll position using the throttle function. Create a button to reset the throttle state if needed. Observe how the log updates only at the start of scrolling or after the delay window closes.
<script>
const log = (msg) => console.log(msg);
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
const handleScroll = throttle((position) => {
log(`Scroll position: ${window.scrollY}`);
}, 200);
window.addEventListener('scroll', () => handleScroll(window.scrollY));
</script>
Scroll the page rapidly. You will see the console log updates only once every 200 milliseconds. If you scroll quickly, the function runs on the first scroll and then waits for the 200ms window before running again. This prevents excessive calculations during high-frequency events.
Verify the implementation
Open the browser console and check the execution count. Type 100 times in the input field within 2 seconds. Without debounce, the function would run 100 times. With debounce, it runs exactly once after you stop typing. Scroll the page 100 times in 2 seconds. With throttle, the function runs roughly 5 times (100 / 200ms), not 100 times.
// Expected console output pattern
// Input test:
// "Searching for: ..." (appears only once after pause)
// Scroll test:
// "Scroll position: 100"
// "Scroll position: 250" (appears roughly every 200ms)
Troubleshooting
If the function executes immediately on load, ensure you are not calling the debounced function directly without an event listener. Verify that the `clearTimeout` is inside the returned function, not the outer function. Check that the delay argument is a positive integer. If the throttle flag does not reset, inspect the `setTimeout` duration to ensure it is not set to zero. Ensure you are using `apply` or `call` to preserve the correct `this` context if the function relies on it.