Debounce vs Throttle in JavaScript

Episode - Debounce vs Throttle in JavaScript — Implement Both!

Hey everyone! Welcome back! Today we are going to learn about two very important performance optimization techniques — Debounce and Throttle!

These are must-know concepts for interviews and real-world frontend development. Both are used to limit how often a function executes, but they work differently!

What we will cover:

  • The Problem — Why do we need Debounce & Throttle?
  • What is Debouncing?
  • Implement Debounce from Scratch
  • What is Throttling?
  • Implement Throttle from Scratch
  • Debounce vs Throttle — Key Differences
  • Real-World Use Cases
  • React Examples
  • Interview Questions

The Problem — Why Do We Need These?

Some events in JavaScript fire way too frequently!

Events that fire rapidly:
==========================

- scroll       → fires 100s of times per second while scrolling
- resize       → fires continuously while resizing window
- mousemove    → fires every pixel the mouse moves
- keyup/input  → fires on every keystroke

Example Problem:
=================

Search Input (like Google):

User types: "JavaScript"

Without Debounce:
  "J"          → API call #1
  "Ja"         → API call #2
  "Jav"        → API call #3
  "Java"       → API call #4
  "JavaS"      → API call #5
  "JavaSc"     → API call #6
  "JavaScr"    → API call #7
  "JavaScri"   → API call #8
  "JavaScrip"  → API call #9
  "JavaScript" → API call #10

10 API calls for ONE search! 😱
That's wasteful and expensive!

With Debounce:
  User stops typing...
  waits 300ms...
  "JavaScript" → API call #1  ✅

Only 1 API call! 🚀

Debounce and Throttle solve this by controlling how often a function runs!


What is Debouncing?

Debounce delays the execution of a function until the user stops performing an action for a specified time period. If the action happens again before the delay is over, the timer resets!

Debounce — Simple Explanation:
===============================

"Wait until the user STOPS doing something, then execute."

Like an elevator door:
- Someone enters → door waits
- Another person enters → door resets timer, waits again
- No one enters for 3 seconds → door closes!

The door only closes when people STOP entering.


Timeline (delay = 300ms):
===========================

Keystroke:  J    a    v    a    S    c    r    i    p    t
            |    |    |    |    |    |    |    |    |    |
Timer:      [start][reset][reset][reset][reset]...     [reset]
                                                           |
                                                      300ms wait...
                                                           |
                                                    ✅ EXECUTE! (once)

Implement Debounce from Scratch

// ═══════════════════════════════════════════════════
//          DEBOUNCE IMPLEMENTATION
// ═══════════════════════════════════════════════════

function debounce(func, delay) {
    let timer;

    return function(...args) {
        // 'this' context is preserved
        const context = this;

        // Clear the previous timer (reset!)
        clearTimeout(timer);

        // Set a new timer
        timer = setTimeout(() => {
            func.apply(context, args);
        }, delay);
    };
}

// Usage:
function searchAPI(query) {
    console.log("API call for:", query);
}

const debouncedSearch = debounce(searchAPI, 300);

// Simulate typing:
debouncedSearch("J");          // Timer starts (300ms)
debouncedSearch("Ja");         // Timer RESETS (300ms again)
debouncedSearch("Jav");        // Timer RESETS (300ms again)
debouncedSearch("Java");       // Timer RESETS (300ms again)
debouncedSearch("JavaScript"); // Timer RESETS (300ms again)
// ... user stops typing ...
// After 300ms → "API call for: JavaScript" ✅ (only ONCE!)

How it works step by step:

Debounce Internals:
====================

1. debounce() returns a NEW function (closure!)
2. 'timer' variable is stored in closure scope
3. Every time the returned function is called:
   a. Clear the previous timer → clearTimeout(timer)
   b. Start a new timer → setTimeout()
4. Only when the delay passes WITHOUT another call,
   the original function executes!

┌─────────────────────────────────────────────┐
│  debounce(func, 300)                        │
│                                             │
│  Closure Scope:                             │
│  ┌─────────────────────┐                    │
│  │ timer = undefined   │                    │
│  └─────────────────────┘                    │
│                                             │
│  Returns: function(...args) {               │
│      clearTimeout(timer);  ← Cancel old     │
│      timer = setTimeout(() => {             │
│          func.apply(this, args); ← Execute  │
│      }, 300);              ← Start new      │
│  }                                          │
└─────────────────────────────────────────────┘

Debounce with Leading Edge (Immediate Execution)

Sometimes you want the function to execute immediately on the first call, then debounce subsequent calls.

// Debounce with leading edge option
function debounce(func, delay, immediate = false) {
    let timer;

    return function(...args) {
        const context = this;
        const callNow = immediate && !timer;

        clearTimeout(timer);

        timer = setTimeout(() => {
            timer = null;
            if (!immediate) {
                func.apply(context, args);
            }
        }, delay);

        // Execute immediately on first call
        if (callNow) {
            func.apply(context, args);
        }
    };
}

// Usage:
const debouncedClick = debounce(handleClick, 300, true);
// First click → executes IMMEDIATELY ✅
// Rapid clicks within 300ms → IGNORED
// After 300ms of no clicks → ready for next immediate execution

What is Throttling?

Throttle ensures a function executes at most once in a specified time interval. No matter how many times the event fires, the function runs only once per interval!

Throttle — Simple Explanation:
===============================

"Execute at most once every X milliseconds."

Like a machine gun with a cooldown:
- You can fire once
- Then you MUST wait for the cooldown
- Even if you keep pressing, it won't fire until cooldown is over
- After cooldown → you can fire again

Like a water tap with a timer:
- Turn it on → water flows
- For the next 1 second → no matter how many times you turn it,
  it won't change
- After 1 second → it can respond again


Timeline (interval = 1000ms):
==============================

Scroll events: ||||||||||||||||||||||||||||||||||||||||
               |         |         |         |
Time:          0ms    1000ms    2000ms    3000ms
               |         |         |         |
Execute:       ✅        ✅        ✅        ✅

Only executes ONCE per second, no matter how many events fire!

Implement Throttle from Scratch

Method 1 — Using Timestamps:

// ═══════════════════════════════════════════════════
//     THROTTLE IMPLEMENTATION (Timestamp approach)
// ═══════════════════════════════════════════════════

function throttle(func, limit) {
    let lastCall = 0;

    return function(...args) {
        const now = Date.now();

        // Only execute if enough time has passed
        if (now - lastCall >= limit) {
            lastCall = now;
            func.apply(this, args);
        }
    };
}

// Usage:
function handleScroll() {
    console.log("Scroll event handled at:", new Date().toLocaleTimeString());
}

const throttledScroll = throttle(handleScroll, 1000);

window.addEventListener('scroll', throttledScroll);
// No matter how fast you scroll,
// handleScroll runs at most ONCE per second!

Method 2 — Using setTimeout (with trailing call):

// ═══════════════════════════════════════════════════
//     THROTTLE IMPLEMENTATION (Timer approach)
// ═══════════════════════════════════════════════════

function throttle(func, limit) {
    let inThrottle = false;

    return function(...args) {
        const context = this;

        if (!inThrottle) {
            // Execute the function
            func.apply(context, args);

            // Set the flag
            inThrottle = true;

            // Reset after the limit
            setTimeout(() => {
                inThrottle = false;
            }, limit);
        }
    };
}

// Usage:
const throttledResize = throttle(() => {
    console.log("Window resized!", window.innerWidth);
}, 500);

window.addEventListener('resize', throttledResize);
// Fires at most once every 500ms during resize

How it works step by step:

Throttle Internals (Timer approach):
=====================================

1. throttle() returns a NEW function (closure!)
2. 'inThrottle' flag is stored in closure scope
3. When the returned function is called:
   a. If inThrottle is FALSE → Execute function, set flag to TRUE
   b. If inThrottle is TRUE → SKIP (do nothing)
   c. After 'limit' ms → Reset flag to FALSE (ready for next call)

┌──────────────────────────────────────────────┐
│  throttle(func, 1000)                        │
│                                              │
│  Closure Scope:                              │
│  ┌──────────────────────┐                    │
│  │ inThrottle = false   │                    │
│  └──────────────────────┘                    │
│                                              │
│  Call 1 (0ms):                               │
│    inThrottle = false → EXECUTE ✅           │
│    inThrottle = true (locked for 1000ms)     │
│                                              │
│  Call 2 (200ms):                             │
│    inThrottle = true → SKIP ❌              │
│                                              │
│  Call 3 (500ms):                             │
│    inThrottle = true → SKIP ❌              │
│                                              │
│  Timer fires (1000ms):                       │
│    inThrottle = false (unlocked!)            │
│                                              │
│  Call 4 (1100ms):                            │
│    inThrottle = false → EXECUTE ✅           │
└──────────────────────────────────────────────┘

Advanced Throttle (Leading + Trailing)

This version executes on both the first call (leading) and the last call (trailing):

function throttle(func, limit) {
    let lastCall = 0;
    let lastArgs = null;
    let timer = null;

    return function(...args) {
        const now = Date.now();
        const context = this;
        const remaining = limit - (now - lastCall);

        if (remaining <= 0) {
            // Leading edge — execute immediately
            if (timer) {
                clearTimeout(timer);
                timer = null;
            }
            lastCall = now;
            func.apply(context, args);
        } else {
            // Trailing edge — save args, execute after remaining time
            lastArgs = args;
            if (!timer) {
                timer = setTimeout(() => {
                    lastCall = Date.now();
                    timer = null;
                    func.apply(context, lastArgs);
                }, remaining);
            }
        }
    };
}

Debounce vs Throttle — Key Differences

Feature Debounce Throttle
When it executes After user STOPS for X ms At most once every X ms
Timer behavior Resets on every call Does NOT reset
Guarantees execution? Only after inactivity At regular intervals
Number of executions 1 (after user stops) Multiple (at intervals)
Best for Search input, form validation Scroll, resize, mouse move
Visual Comparison:
===================

Events:    | | | | | | | | | |          | | | | |
           ─────────────────────────────────────────→ time

Debounce (300ms):
           . . . . . . . . . .    ✅    . . . . .    ✅
                                   ↑                    ↑
                            After STOP            After STOP

Throttle (300ms):
           ✅ . . ✅ . . ✅ . . ✅      ✅ . . ✅ . ✅
           ↑      ↑      ↑      ↑       ↑      ↑     ↑
        Every 300ms interval          Every 300ms interval


Think of it this way:
======================

Debounce = "Wait until I'm DONE, then do it once"
           Like waiting to send a message until you finish typing

Throttle = "Do it now, then wait before doing it again"
           Like a speed limit — you can only go so fast

Real-World Use Cases

When to use DEBOUNCE:

  • Search input — Wait until user stops typing, then call API
  • Form validation — Validate after user finishes entering data
  • Auto-save — Save draft after user stops editing
  • Window resize — Recalculate layout after user stops resizing
  • Autocomplete/suggestions — Fetch suggestions after pause

When to use THROTTLE:

  • Scroll events — Infinite scroll, lazy loading, parallax effects
  • Mouse move — Drag and drop, drawing, tooltip positioning
  • Button clicks — Prevent double-submit on rapid clicks
  • API rate limiting — Ensure you don't exceed rate limits
  • Game loop — Limit FPS or action frequency
  • Analytics tracking — Send scroll/interaction data at intervals

React Examples

1. Debounce in React — Search Input:

import { useState, useCallback } from 'react';

// Custom debounce hook
function useDebounce(func, delay) {
    const timerRef = useRef(null);

    return useCallback((...args) => {
        clearTimeout(timerRef.current);
        timerRef.current = setTimeout(() => {
            func(...args);
        }, delay);
    }, [func, delay]);
}

// Usage in component
function SearchComponent() {
    const [results, setResults] = useState([]);

    const searchAPI = async (query) => {
        const response = await fetch(`/api/search?q=${query}`);
        const data = await response.json();
        setResults(data);
    };

    const debouncedSearch = useDebounce(searchAPI, 300);

    return (
        <input
            type="text"
            placeholder="Search..."
            onChange={(e) => debouncedSearch(e.target.value)}
        />
    );
}

2. Throttle in React — Scroll Handler:

import { useEffect, useRef, useCallback } from 'react';

// Custom throttle hook
function useThrottle(func, limit) {
    const inThrottleRef = useRef(false);

    return useCallback((...args) => {
        if (!inThrottleRef.current) {
            func(...args);
            inThrottleRef.current = true;
            setTimeout(() => {
                inThrottleRef.current = false;
            }, limit);
        }
    }, [func, limit]);
}

// Usage in component
function InfiniteScrollComponent() {
    const [items, setItems] = useState([]);

    const loadMore = () => {
        console.log("Loading more items...");
        // Fetch next page of data
    };

    const throttledLoadMore = useThrottle(loadMore, 1000);

    useEffect(() => {
        const handleScroll = () => {
            const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
            if (scrollTop + clientHeight >= scrollHeight - 100) {
                throttledLoadMore();
            }
        };

        window.addEventListener('scroll', handleScroll);
        return () => window.removeEventListener('scroll', handleScroll);
    }, [throttledLoadMore]);

    return <div>{/* render items */}</div>;
}

3. Using lodash (Production Shortcut):

import { debounce, throttle } from 'lodash';

// Debounce
const debouncedSearch = debounce((query) => {
    fetch(`/api/search?q=${query}`);
}, 300);

// Throttle
const throttledScroll = throttle(() => {
    console.log("Scrolled!");
}, 1000);

// In production, lodash is battle-tested and handles edge cases.
// But for interviews, you MUST know how to implement from scratch!

Common Mistakes to Avoid

Mistake #1: Creating debounce/throttle inside render
=====================================================

// ❌ BAD — Creates new function on EVERY render!
function SearchComponent() {
    const debouncedSearch = debounce(searchAPI, 300); // New function each render!
    return <input onChange={(e) => debouncedSearch(e.target.value)} />;
}

// ✅ GOOD — Use useCallback or useRef to maintain reference
function SearchComponent() {
    const debouncedSearch = useCallback(
        debounce(searchAPI, 300),
        []
    );
    return <input onChange={(e) => debouncedSearch(e.target.value)} />;
}


Mistake #2: Not cleaning up timers
====================================

// ❌ BAD — Timer leaks on component unmount!
useEffect(() => {
    window.addEventListener('scroll', throttledScroll);
}, []);

// ✅ GOOD — Clean up!
useEffect(() => {
    window.addEventListener('scroll', throttledScroll);
    return () => {
        window.removeEventListener('scroll', throttledScroll);
        throttledScroll.cancel?.(); // Cancel pending execution
    };
}, []);


Mistake #3: Confusing debounce and throttle
=============================================

// ❌ Using throttle for search input
// (user gets results while still typing — wasteful!)

// ❌ Using debounce for scroll events
// (user scrolls for 10 seconds, nothing happens until they stop!)

// ✅ Search input → DEBOUNCE (wait until done typing)
// ✅ Scroll events → THROTTLE (execute at intervals)

Interview Questions

Q1: What is debouncing in JavaScript?

"Debouncing is a technique that delays the execution of a function until the user stops performing an action for a specified period. If the action occurs again before the delay completes, the timer resets. It's commonly used for search inputs — we wait until the user finishes typing before making an API call, instead of calling on every keystroke."

Q2: What is throttling in JavaScript?

"Throttling limits a function to execute at most once in a given time interval, regardless of how many times the event fires. For example, during scroll events that fire hundreds of times per second, throttle ensures our handler runs only once per second, improving performance without missing updates."

Q3: What is the difference between debounce and throttle?

"Debounce waits until the user STOPS performing an action, then executes once. The timer resets on each call. Throttle executes at regular intervals regardless of how many times the event fires — the timer does NOT reset. Use debounce for search inputs (wait until done typing), use throttle for scroll events (execute at regular intervals)."

Q4: Can you implement debounce from scratch?

"Yes. The debounce function takes a callback and delay, returns a new function using closures. Inside, we store a timer variable. On each call, we clearTimeout the previous timer and set a new setTimeout. The original function only executes when the delay passes without another call."

Q5: Can you implement throttle from scratch?

"Yes. Using the flag approach: we store an 'inThrottle' boolean in closure. If false, we execute the function and set it to true. A setTimeout resets it to false after the limit. While it's true, all calls are skipped. Using timestamps: we compare Date.now() with the last execution time and only execute if the difference exceeds the limit."

Q6: Why do we use closures in debounce/throttle implementations?

"Closures allow the returned function to remember and access the timer variable (or flag/timestamp) across multiple calls. Without closures, the timer would be re-created on every call and we couldn't clear the previous one. The closure preserves state between function invocations."

Q7: How do you handle debounce/throttle in React?

"In React, you must ensure the debounced/throttled function reference doesn't change on re-renders. Use useRef to store the timer, or useCallback with the debounce function, or create a custom hook like useDebounce. Never create the debounced function directly inside the component body — it creates a new instance on every render, breaking the debounce logic."


Quick Recap

Concept Description
Debounce Execute after user STOPS for X ms. Timer resets on each call.
Throttle Execute at most once every X ms. Timer does NOT reset.
Debounce Use Search input, form validation, auto-save, autocomplete
Throttle Use Scroll, resize, mouse move, button clicks, API rate limiting
Key Mechanism Both use closures to persist timer/state across calls
In React Use useRef/useCallback to prevent re-creation on renders

Key Points to Remember

  • Debounce = "Wait until user STOPS, then execute once"
  • Throttle = "Execute now, then wait before allowing again"
  • Debounce resets timer on every call, Throttle does NOT
  • Both use closures to maintain state across calls
  • Both use setTimeout and clearTimeout
  • Search input → Debounce
  • Scroll/resize → Throttle
  • In React, use useRef or useCallback to preserve reference
  • Never create debounce/throttle inside render — it breaks!
  • In production, lodash provides battle-tested implementations
  • For interviews, you MUST know how to implement from scratch

Keep coding, keep learning! See you in the next one!