How to debounce a function in vanilla JavaScript
Learn how to implement a debounce utility in vanilla JavaScript to optimize performance by limiting how often a function executes during rapid events.
Debounce functions prevent excessive execution of event handlers by delaying the call until a specified time has passed since the last event. This technique improves performance on expensive operations like window resizing or search input handling. The following steps show you how to create a reusable debounce utility and apply it to real-world scenarios.
Prerequisites
- A modern browser with support for ES6 features (arrow functions, let/const).
- Basic knowledge of JavaScript closures and asynchronous operations.
- A code editor or an online playground like CodePen or JSFiddle.
Step 1: Create the debounce function
Define a function that accepts a target function, a delay in milliseconds, and an optional boolean flag to determine if the function should execute immediately upon the first call.
function debounce(func, delay, immediate = false) {
let timer;
return function (...args) {
const context = this;
clearTimeout(timer);
timer = setTimeout(() => {
if (immediate) {
func.apply(context, args);
} else {
func.apply(context, args);
}
}, delay);
};
}
You will see that the returned function clears any existing timer before starting a new one. This ensures that only the most recent event triggers the final execution after the delay expires.
Step 2: Apply debounce to a window resize handler
Create a standard function that logs the window width and height. Then, wrap it with the debounce utility using a delay of 250 milliseconds. Assign the debounced version to the window's resize event listener.
const handleResize = () => {
console.log(`Window resized: ${window.innerWidth} x ${window.innerHeight}`);
};
const debouncedResize = debounce(handleResize, 250);
window.addEventListener('resize', debouncedResize);
When you resize the browser window rapidly, the console will only update after the resizing stops for 250 milliseconds. This prevents the browser from recalculating layout thousands of times per second.
Step 3: Implement immediate execution mode
Some scenarios require the function to run immediately on the first event, then wait for subsequent events to expire. Pass true as the third argument to the debounce function to enable immediate execution.
const immediateResize = debounce(handleResize, 250, true);
window.addEventListener('resize', immediateResize);
In this configuration, the function runs instantly when the resize starts. If you continue resizing, the previous execution is cancelled. The function runs again only if the delay passes without further events.
Step 4: Debounce search input events
Create an input element and a function that simulates an expensive API call by logging a message. Attach the debounced handler to the input's 'input' event with a 500-millisecond delay.
const searchInput = document.getElementById('search');
const searchHandler = () => {
console.log('Searching for:', searchInput.value);
};
const debouncedSearch = debounce(searchHandler, 500);
searchInput.addEventListener('input', debouncedSearch);
As you type in the input field, the search will not trigger until you stop typing for half a second. This reduces unnecessary network requests and improves user experience.
Verify the installation
Open your browser's Developer Tools and navigate to the Console tab. Resize the window or type in the search box. You will see that the log messages appear only after the delay expires, confirming the debounce logic is working correctly.
Console Output Example:
Window resized: 1920 x 1080
Window resized: 1920 x 1080
Window resized: 1920 x 1080
Notice that even though you might trigger the event multiple times, the output matches the number of distinct delay intervals that completed.
Troubleshooting
Error: "timer is not defined"
This error occurs if you forget to declare the timer variable inside the debounce function scope. Ensure the variable is declared with let at the top of the function before using it in clearTimeout.
Error: "func is not a function"
This happens when you pass a non-function argument to the debounce utility. Verify that the first argument passed to debounce is indeed a function using typeof func === 'function' before wrapping it.
Issue: Function executes immediately on every event
If the function runs on every single event, you may have forgotten to call clearTimeout before setting a new timer. The previous timer must be cleared to prevent the old one from firing after the delay.
Issue: Debounce stops working after page reload
This is not a bug but a common misunderstanding. The debounce closure retains the timer reference. If you reassign the debounced function without clearing the old timer, the old timer might still be active. Always create a fresh debounced function for each new event listener setup.