CrackFrontendCF
Resources
Practice
CrackFrontend

Your guide to mastering frontend interviews

Resources

LibraryPracticeDonate

Company

About UsContact

Legal

Privacy PolicyTerms of Service

Built with ❤️ by Tushar Khanna

CrackFrontendCF
Resources
Practice

⏱️ Debounce Function in JavaScript

Delays function execution until a pause in calls. Essential for search inputs, resize handlers, and reducing API calls.

Interview Importance: 🔴 Critical — One of the most commonly asked JavaScript interview questions. Expected at every level from junior to senior. -- 1️⃣ What is Debounce? Debounce is a technique that delays the execution of a function until a certain amount of time has passed since the last time it was invoked. If the function is called again before the delay completes, the timer resets. Real-World Analogy Think of an elevator door: When someone enters, the door waits a few seconds before closing If another person enters during that wait, the timer resets The door only closes after no one has entered for the full delay period -- 2️⃣ Why Use Debounce? Common Use Cases Problem Without Debounce API call on every keystroke Layout calculation on every pixel Multiple submissions on rapid clicks Save on every character Hundreds of events per scroll Performance Impact -- 3️⃣ How Debounce Works — Implementation Basic Implementation 🔍 Dry Run: Step-by-Step Execution Result: Only is logged, 1000ms after the last call. -- 4️⃣ Understanding Key Concepts Why ? Why ? Preserves the correct context and passes arguments: Why Return a Function (Closure)? The returned function "remembers" between calls: -- 5️⃣ Advanced Implementations With Immediate/Leading Edge Option Sometimes you want to execute immediately on first call, then debounce subsequent calls: With Cancel and Flush Methods With Max Wait (Throttle Fallback) Ensures function is called at least once every ms: -- 6️⃣ React Implementations Using useCallback useRef Debounced Value Hook -- 7️⃣ Debounce vs Throttle Debounce After a pause in calls Once after delay Search input, auto-save Waits for silence Visual Comparison -- 8️⃣ Common Interview Questions Q1: Implement a debounce function Answer: See Section 3 for basic implementation. Q2: What happens if you call debounce without clearing the timeout? Answer: Every call would schedule a new execution, defeating the purpose. All calls would eventually fire: Q3: How would you test a debounced function? Answer: Use fake timers (Jest): Q4: What's wrong with this implementation? Answer: Arrow function doesn't preserve context from caller. Should use keyword and : Q5: Implement debounce with leading and trailing options Answer: See Section 5 "With Immediate/Leading Edge Option". -- 9️⃣ Common Pitfalls Pitfall 1: Creating New Debounced Function on Every Render Pitfall 2: Not Cleaning Up on Unmount Pitfall 3: Losing Context -- 🔟 Time Complexity Analysis Time Complexity O(1) O(1) O(1) O(1) The debounce function itself is O(1) — the complexity of is separate. -- Summary Description Delays execution until calls stop for specified duration Timer Reset Execute immediately on first call Trailing Edge Search, auto-save, resize handlers | Key Takeaways 1. Debounce waits for a pause — ideal for search inputs 2. Always clear previous timeout — otherwise all calls will fire 3. Preserve context — use regular functions, not arrows 4. Clean up in React — cancel timers on unmount 5. Know the difference from throttle — debounce waits, throttle limits -- 📚 Further Reading Lodash debounce documentation CSS-Tricks: Debouncing and Throttling Explained -- <!-quiz-start --Q1: What happens if you don't call in a debounce implementation? [ ] The function will never execute [ ] Memory leak will occur [x] All calls will eventually fire, defeating the purpose of debounce [ ] The delay will be doubled Q2: Which scenario is BEST suited for debounce (not throttle)? [ ] Tracking scroll position for infinite scroll [x] Auto-saving a document after user stops typing [ ] Rate-limiting API requests per second [ ] Updating mouse position on mousemove Q3: Why should you use a regular function (not arrow function) as the returned debounced function? [ ] Arrow functions are slower [ ] Arrow functions cannot access arguments [x] Arrow functions don't have their own context [ ] Arrow functions cannot use setTimeout <!-quiz-end --
JavaScriptUtilities
➕ Chained Sum (Curried Function)
medium
⏱️ Debounce Function in JavaScript
medium
📋 Deep Clone Implementation
easy
🔄 distinctUntilChanged() Polyfill
easy
📄 Document Comparison (Diff)
easy
📢 Custom EventEmitter Implementation
hard
📦 Flatten Object Implementation
medium
🐫➡️🐍 Converting camelCase to snake_case in JavaScript (Without Regex)
easy
🔄 mapLimit: Controlled Concurrency in JavaScript
medium
⚡️ Fire on Push: Dispatching Custom Events When an Array Changes in JavaScript
medium
🔄 Removing Circular References from Objects
hard
📊 Sampling Function: Execute Once Every N Calls
medium
⏱️ Throttle Function in JavaScript
medium
🔄 undefinedToNull Utility
medium
2 of 14
LibraryJavaScriptUtilities49 of 61

⏱️ Debounce Function in JavaScript

jsutilsmedium

Interview Importance: 🔴 Critical — One of the most commonly asked JavaScript interview questions. Expected at every level from junior to senior.


1️⃣ What is Debounce?

Debounce is a technique that delays the execution of a function until a certain amount of time has passed since the last time it was invoked. If the function is called again before the delay completes, the timer resets.

Real-World Analogy

Think of an elevator door:

  • When someone enters, the door waits a few seconds before closing
  • If another person enters during that wait, the timer resets
  • The door only closes after no one has entered for the full delay period
Without Debounce:          With Debounce:
-----------------          -----------------
User types: H              User types: H
  -> API call               User types: e
User types: e              User types: l
  -> API call               User types: l
User types: l              User types: o
  -> API call               ... waits 300ms ...
User types: l                -> ONE API call for "Hello"
  -> API call
User types: o
  -> API call

5 API calls!               1 API call!

2️⃣ Why Use Debounce?

Common Use Cases

Use CaseProblem Without DebounceSolution With Debounce
Search InputAPI call on every keystrokeAPI call after user stops typing
Window ResizeLayout calculation on every pixelCalculate after resize ends
Button ClickMultiple submissions on rapid clicksSingle submission after clicks stop
Auto-saveSave on every characterSave after user pauses typing
Scroll EventsHundreds of events per scrollProcess after scroll stops

Performance Impact

// Typing "hello world" triggers:
// Without debounce: 11 function calls
// With 300ms debounce: 1-2 function calls (after pauses)

3️⃣ How Debounce Works — Implementation

Basic Implementation

function debounce(fn, delay) {
  let timerId;

  return function(...args) {
    // Clear any existing timer
    clearTimeout(timerId);

    // Set a new timer
    timerId = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

🔍 Dry Run: Step-by-Step Execution

const log = (msg) => console.log(msg);
const debouncedLog = debounce(log, 1000);

// Timeline of calls:
debouncedLog("A");   // t = 0ms
debouncedLog("B");   // t = 300ms
debouncedLog("C");   // t = 600ms
// ... user stops typing ...
// Output at t = 1600ms: "C"
Time     | Action                           | timerId State
---------|----------------------------------|------------------
0ms      | debouncedLog("A") called         | timerId = undefined
         | clearTimeout(undefined) -> no-op  |
         | setTimeout -> timer1 for t=1000ms | timerId = timer1
         |                                  |
300ms    | debouncedLog("B") called         |
         | clearTimeout(timer1) -> CANCELLED |
         | setTimeout -> timer2 for t=1300ms | timerId = timer2
         |                                  |
600ms    | debouncedLog("C") called         |
         | clearTimeout(timer2) -> CANCELLED |
         | setTimeout -> timer3 for t=1600ms | timerId = timer3
         |                                  |
1000ms   | timer1 would fire (CANCELLED)    | (already cancelled)
1300ms   | timer2 would fire (CANCELLED)    | (already cancelled)
1600ms   | timer3 FIRES! log("C") executes  | Output: "C"

Result: Only "C" is logged, 1000ms after the last call.


4️⃣ Understanding Key Concepts

Why clearTimeout(timerId)?

// Without clearTimeout:
debouncedLog("A");  // Creates timer for A
debouncedLog("B");  // Creates timer for B
debouncedLog("C");  // Creates timer for C
// Result: A, B, C all logged (defeats purpose!)

// With clearTimeout:
debouncedLog("A");  // Creates timer for A
debouncedLog("B");  // Cancels A, creates timer for B
debouncedLog("C");  // Cancels B, creates timer for C
// Result: Only C logged after delay

Why fn.apply(this, args)?

Preserves the correct this context and passes arguments:

const obj = {
  name: "Button",
  handleClick: debounce(function(event) {
    console.log(this.name);  // "Button" - correct context
    console.log(event.type); // "click" - arguments passed
  }, 300)
};

button.addEventListener("click", obj.handleClick.bind(obj));

Why Return a Function (Closure)?

The returned function "remembers" timerId between calls:

function debounce(fn, delay) {
  let timerId;  // This variable persists across calls

  return function(...args) {
    // Each call accesses the SAME timerId
    clearTimeout(timerId);
    timerId = setTimeout(() => fn.apply(this, args), delay);
  };
}

5️⃣ Advanced Implementations

With Immediate/Leading Edge Option

Sometimes you want to execute immediately on first call, then debounce subsequent calls:

function debounce(fn, delay, { leading = false, trailing = true } = {}) {
  let timerId;
  let lastArgs;

  return function(...args) {
    const shouldCallImmediately = leading && !timerId;

    lastArgs = args;
    clearTimeout(timerId);

    if (shouldCallImmediately) {
      fn.apply(this, args);
    }

    timerId = setTimeout(() => {
      if (trailing && lastArgs) {
        fn.apply(this, lastArgs);
      }
      timerId = null;
      lastArgs = null;
    }, delay);
  };
}

// Usage:
const logLeading = debounce(console.log, 1000, { leading: true, trailing: false });
logLeading("A");  // Logs immediately
logLeading("B");  // Ignored
logLeading("C");  // Ignored
// After 1000ms of inactivity, can trigger again

With Cancel and Flush Methods

function debounce(fn, delay) {
  let timerId;
  let lastArgs;
  let lastThis;

  function debounced(...args) {
    lastArgs = args;
    lastThis = this;
    clearTimeout(timerId);

    timerId = setTimeout(() => {
      fn.apply(lastThis, lastArgs);
      timerId = null;
    }, delay);
  }

  // Cancel pending execution
  debounced.cancel = function() {
    clearTimeout(timerId);
    timerId = null;
    lastArgs = null;
    lastThis = null;
  };

  // Execute immediately
  debounced.flush = function() {
    if (timerId) {
      clearTimeout(timerId);
      fn.apply(lastThis, lastArgs);
      timerId = null;
    }
  };

  return debounced;
}

// Usage:
const debouncedSave = debounce(save, 1000);
debouncedSave(data);

// User navigates away - save immediately
debouncedSave.flush();

// Or cancel if not needed
debouncedSave.cancel();

With Max Wait (Throttle Fallback)

Ensures function is called at least once every maxWait ms:

function debounce(fn, delay, maxWait = Infinity) {
  let timerId;
  let lastCallTime = 0;

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

    clearTimeout(timerId);

    // If maxWait exceeded, execute immediately
    if (timeSinceLastCall >= maxWait) {
      lastCallTime = now;
      fn.apply(this, args);
      return;
    }

    timerId = setTimeout(() => {
      lastCallTime = Date.now();
      fn.apply(this, args);
    }, delay);
  };
}

// Usage: Debounce with max 2 second wait
const debouncedScroll = debounce(handleScroll, 300, 2000);

6️⃣ React Implementations

Using useCallback + useRef

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

function useDebounce(callback, delay) {
  const timeoutRef = useRef(null);
  const callbackRef = useRef(callback);

  // Update callback ref when callback changes
  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  // Cleanup on unmount
  useEffect(() => {
    return () => clearTimeout(timeoutRef.current);
  }, []);

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

// Usage:
function SearchComponent() {
  const [query, setQuery] = useState('');

  const debouncedSearch = useDebounce((searchTerm) => {
    console.log('Searching for:', searchTerm);
    // API call here
  }, 300);

  const handleChange = (e) => {
    setQuery(e.target.value);
    debouncedSearch(e.target.value);
  };

  return <input value={query} onChange={handleChange} />;
}

Debounced Value Hook

import { useState, useEffect } from 'react';

function useDebouncedValue(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timerId = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(timerId);
  }, [value, delay]);

  return debouncedValue;
}

// Usage:
function SearchComponent() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebouncedValue(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      // API call with debouncedQuery
      console.log('Searching for:', debouncedQuery);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

7️⃣ Debounce vs Throttle

AspectDebounceThrottle
When it firesAfter a pause in callsAt regular intervals
FrequencyOnce after delayAt most once per delay
Use caseSearch input, auto-saveScroll, resize, rate limiting
BehaviorWaits for silenceLimits frequency

Visual Comparison

Events:     --●--●●●●●------●●●------●----------●--------
            0  50        500       700        900ms

Debounce (200ms):
            -----------------●-------------●------------●
                            500ms        900ms       1100ms
            (fires 200ms after last event in each burst)

Throttle (200ms):
            --●----●----●----●----●----●----●----●------
            0   200  400  500  700  800  900 1000ms
            (fires at most every 200ms)

8️⃣ Common Interview Questions

Q1: Implement a debounce function

Answer: See Section 3 for basic implementation.

Q2: What happens if you call debounce without clearing the timeout?

Answer: Every call would schedule a new execution, defeating the purpose. All calls would eventually fire:

// Bad: No clearTimeout
function badDebounce(fn, delay) {
  return function(...args) {
    setTimeout(() => fn.apply(this, args), delay);  // No clearing!
  };
}

// Calling badDebounce("A"), ("B"), ("C") rapidly
// Results in: "A", "B", "C" all firing after delay

Q3: How would you test a debounced function?

Answer: Use fake timers (Jest):

jest.useFakeTimers();

test('debounce delays execution', () => {
  const fn = jest.fn();
  const debounced = debounce(fn, 1000);

  debounced('a');
  debounced('b');
  debounced('c');

  expect(fn).not.toHaveBeenCalled();

  jest.advanceTimersByTime(1000);

  expect(fn).toHaveBeenCalledTimes(1);
  expect(fn).toHaveBeenCalledWith('c');
});

Q4: What's wrong with this implementation?

function debounce(fn, delay) {
  let timerId;
  return (...args) => {
    clearTimeout(timerId);
    timerId = setTimeout(() => fn(...args), delay);
  };
}

Answer: Arrow function doesn't preserve this context from caller. Should use function keyword and fn.apply(this, args):

// Fix:
function debounce(fn, delay) {
  let timerId;
  return function(...args) {  // Regular function
    clearTimeout(timerId);
    timerId = setTimeout(() => fn.apply(this, args), delay);
  };
}

Q5: Implement debounce with leading and trailing options

Answer: See Section 5 "With Immediate/Leading Edge Option".


9️⃣ Common Pitfalls

Pitfall 1: Creating New Debounced Function on Every Render

// ❌ BAD: New debounced function each render
function SearchComponent() {
  const debouncedSearch = debounce(search, 300);  // Recreated every render!
  return <input onChange={(e) => debouncedSearch(e.target.value)} />;
}

// ✅ GOOD: Stable reference
function SearchComponent() {
  const debouncedSearch = useMemo(
    () => debounce(search, 300),
    []
  );
  return <input onChange={(e) => debouncedSearch(e.target.value)} />;
}

Pitfall 2: Not Cleaning Up on Unmount

// ❌ BAD: Timer might fire after unmount
useEffect(() => {
  const debouncedFn = debounce(updateState, 300);
  window.addEventListener('resize', debouncedFn);
  return () => window.removeEventListener('resize', debouncedFn);
  // Timer might still fire!
}, []);

// ✅ GOOD: Cancel on cleanup
useEffect(() => {
  const debouncedFn = debounce(updateState, 300);
  window.addEventListener('resize', debouncedFn);
  return () => {
    window.removeEventListener('resize', debouncedFn);
    debouncedFn.cancel?.();  // Cancel pending timer
  };
}, []);

Pitfall 3: Losing this Context

// ❌ BAD: Arrow function loses `this`
const obj = {
  name: 'Test',
  log: debounce(() => {
    console.log(this.name);  // undefined - wrong `this`
  }, 300)
};

// ✅ GOOD: Regular function preserves `this`
const obj = {
  name: 'Test',
  log: debounce(function() {
    console.log(this.name);  // "Test" - correct
  }, 300)
};

🔟 Time Complexity Analysis

OperationTime ComplexitySpace Complexity
Call debounced functionO(1)O(1)
clearTimeoutO(1)-
setTimeoutO(1)-
OverallO(1)O(1)

The debounce function itself is O(1) — the complexity of fn is separate.


Summary

ConceptDescription
DebounceDelays execution until calls stop for specified duration
Timer ResetEach call cancels previous timer and starts new one
Leading EdgeExecute immediately on first call
Trailing EdgeExecute after delay (default behavior)
Use CasesSearch, auto-save, resize handlers

Key Takeaways

  1. Debounce waits for a pause — ideal for search inputs
  2. Always clear previous timeout — otherwise all calls will fire
  3. Preserve this context — use regular functions, not arrows
  4. Clean up in React — cancel timers on unmount
  5. Know the difference from throttle — debounce waits, throttle limits

📚 Further Reading

  • Lodash debounce documentation
  • CSS-Tricks: Debouncing and Throttling Explained

Quick Quiz

Test your understanding with 3 quick questions

Q1What happens if you don't call `clearTimeout` in a debounce implementation?
Q2Which scenario is BEST suited for debounce (not throttle)?
Q3Why should you use a regular function (not arrow function) as the returned debounced function?

Continue Reading

CrackFrontend

Your guide to mastering frontend interviews

Resources

LibraryPracticeDonate

Company

About UsContact

Legal

Privacy PolicyTerms of Service

Built with ❤️ by Tushar Khanna