I once built a search feature that made an API call on every keystroke. Seemed fine in development with my fast typing. Then a user typed "javascript frameworks" and our server got hammered with 21 requests in about 2 seconds. We got rate-limited by our own API.

That's when I learned about debouncing and throttling. Same problem, two different solutions. Let me explain when to use which.

The Problem: Too Many Events

Some events fire way more often than you need:

If your handler does anything expensive (API call, DOM manipulation, complex calculation), you'll destroy performance.

Debounce: Wait Until They're Done

Think of an elevator. Someone enters, the doors wait 3 seconds to close. Someone else enters at second 2, the timer resets. The doors only close after 3 seconds of no one entering.

Debouncing waits until the user stops doing something, then fires once.

function debounce(fn, delay) {
    let timeoutId;
    
    return function(...args) {
        // Clear any existing timer
        clearTimeout(timeoutId);
        
        // Set a new timer
        timeoutId = setTimeout(() => {
            fn.apply(this, args);
        }, delay);
    };
}

// Usage
const searchInput = document.getElementById('search');

const handleSearch = debounce((query) => {
    console.log('Searching for:', query);
    // API call here
}, 300);

searchInput.addEventListener('input', (e) => {
    handleSearch(e.target.value);
});

User types "react"... nothing happens. They pause for 300ms... NOW the search fires. One API call instead of five.

Use debounce for: Search inputs, form validation, save-on-change features, resize handlers that recalculate layout.

Throttle: Limit the Rate

Now imagine a bouncer at a club. He lets one person in every 5 seconds, no matter how many are waiting. Constant, predictable flow.

Throttling guarantees a function runs at most once every X milliseconds. Unlike debounce, it fires during the action, not just at the end.

function throttle(fn, limit) {
    let inThrottle = false;
    
    return function(...args) {
        if (!inThrottle) {
            fn.apply(this, args);
            inThrottle = true;
            
            setTimeout(() => {
                inThrottle = false;
            }, limit);
        }
    };
}

// Usage
const handleScroll = throttle(() => {
    console.log('Scroll position:', window.scrollY);
    // Update progress bar, lazy load images, etc.
}, 100);

window.addEventListener('scroll', handleScroll);

User scrolls like crazy... function fires every 100ms steadily. You get regular updates without being overwhelmed.

Use throttle for: Scroll handlers, drag handlers, game loops, rate-limited APIs, anything where you need regular updates during continuous action.

Quick Comparison

Scenario Debounce Throttle
Search autocomplete
Scroll progress bar
Window resize layout
Infinite scroll loading
Form auto-save
Button spam prevention

requestAnimationFrame for Animations

For anything visual (scroll effects, animations), there's a third option: requestAnimationFrame. It syncs with the browser's repaint cycle (~60fps).

let ticking = false;

window.addEventListener('scroll', () => {
    if (!ticking) {
        requestAnimationFrame(() => {
            updateParallax(window.scrollY);
            ticking = false;
        });
        ticking = true;
    }
});

This is basically throttling to 60fps, but synced with when the browser actually renders. Perfect for visual updates.

Real World: Leading vs Trailing

Sometimes you want the function to fire immediately on the first event, then debounce/throttle subsequent calls. Lodash calls this "leading" vs "trailing" edge.

function debounce(fn, delay, immediate = false) {
    let timeoutId;
    
    return function(...args) {
        const callNow = immediate && !timeoutId;
        
        clearTimeout(timeoutId);
        
        timeoutId = setTimeout(() => {
            timeoutId = null;
            if (!immediate) fn.apply(this, args);
        }, delay);
        
        if (callNow) fn.apply(this, args);
    };
}

// Fire immediately on first click, ignore rapid subsequent clicks
const handleClick = debounce(submitForm, 1000, true);

Should You Use Lodash?

Lodash's _.debounce and _.throttle are battle-tested and handle edge cases I haven't shown (cancel, flush, maxWait). If you're already using Lodash, use them.

But for simple cases, the implementations above work fine. No need to add a dependency for 15 lines of code.

The important thing is understanding the concept. Once you recognize "this event fires too often," you'll know which tool to reach for.

← Back to JavaScript Articles

Back to Home