Chapter 9.6โ˜• 40 min read

Debounce, Throttle & requestAnimationFrame

Har keypress pe API? Server doob gaya. Har scroll pe handler? JANK! Debounce = rukne pe call. Throttle = har X ms pe call. rAF = screen ke saath sync.

01๐ŸŽฏ Try/Catch & The Scope Quirk

Debounce waits X milliseconds after the last call before executing. If called again within X ms, it restarts the timer. The function only fires once โ€” after the caller stops.

Use for: Search input (wait until user stops typing), window resize (wait until resizing stops), form auto-save.

Implementation: clearTimeout + setTimeout โ€” each call clears the previous timer and starts a new one.

// Basic debounce implementation
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer); // Clear previous timer on every call
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// Usage: Search input
const searchInput = document.getElementById("search");
const handleSearch = debounce(async (query) => {
  console.log("Searching:", query);
  const results = await fetch("/api/search?q=" + query);
  // Only fires 300ms after user STOPS typing!
}, 300);

searchInput.addEventListener("input", (e) => handleSearch(e.target.value));
// User types "hello" quickly โ†’ fires ONCE after 300ms, not 5 times!

Debounce with cancel and flush: Return an object with .cancel() to clear the pending timer and .flush() to immediately execute.

function advancedDebounce(fn, delay) {
  let timer;
  const debounced = function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
  debounced.cancel = () => clearTimeout(timer);
  debounced.flush = (...args) => { clearTimeout(timer); fn(...args); };
  return debounced;
}

Rule: Debounce = "Wait until they STOP doing it, then act once."

02๐Ÿ”ง Custom Errors โ€” Extending Error Class

Throttle executes at most once every X ms. First call executes immediately, then subsequent calls are ignored until X ms passes.

Use for: Scroll handlers, mouse move tracking, button click spam prevention, API rate limiting.

Implementation: Track an inThrottle flag + setTimeout to reset it after the limit period.

// Basic throttle implementation
function throttle(fn, limit) {
  let inThrottle = false;
  return function(...args) {
    if (!inThrottle) {
      fn.apply(this, args); // Execute immediately on first call
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit); // Reset after limit
    }
    // Calls during inThrottle are SILENTLY IGNORED
  };
}

// Usage: Scroll handler
const handleScroll = throttle(() => {
  console.log("Scroll position:", window.scrollY);
  updateLazyImages(); // Fires at most every 100ms while scrolling
}, 100);

window.addEventListener("scroll", handleScroll);
// Without throttle: scroll fires 60+ times per second!
// With throttle: fires at most 10 times per second (100ms intervals).

Throttle with trailing edge: If calls were made during the throttle period, execute ONE more time after the period ends with the latest arguments.

function trailingThrottle(fn, limit) {
  let inThrottle = false;
  let lastArgs = null;
  return function(...args) {
    if (!inThrottle) {
      fn.apply(this, args);
      inThrottle = true;
      setTimeout(() => {
        inThrottle = false;
        if (lastArgs) { // If there were calls during throttle
          fn(...lastArgs); // Fire one more time with latest args
          lastArgs = null;
        }
      }, limit);
    } else {
      lastArgs = args; // Save latest args for trailing edge
    }
  };
}
setTimeout precision: setTimeout has a minimum delay of ~4ms (after 5th nested timeout per HTML spec) and is NOT precise โ€” it can fire late if the main thread is busy. For visual updates, requestAnimationFrame is always better because it is synced to the display refresh rate and automatically pauses when the tab is hidden.

Debounce vs Throttle: Debounce = "wait until STOP". Throttle = "fire at most every X ms WHILE doing it". Search input โ†’ debounce. Scroll handler โ†’ throttle.

03๐Ÿงช Error Boundaries via Closure

requestAnimationFrame (rAF) schedules a callback to run before the next browser repaint โ€” synced to display refresh (typically 60fps = ~16.6ms per frame).

Why rAF over setTimeout for visual updates: setTimeout has a 4ms minimum delay, is not synced to refresh, and causes visual jank. rAF fires exactly before each paint.

rAF is automatically paused when the tab is hidden โ€” saves battery and CPU. setTimeout keeps running in background tabs!

// requestAnimationFrame for smooth animations
function animate() {
  element.style.transform = "translateX(" + position + "px)";
  position += speed;
  if (position < target) {
    requestAnimationFrame(animate); // Schedule next frame
  }
}
requestAnimationFrame(animate); // Start animation

// rAF throttle โ€” only update visuals once per frame
function rafThrottle(fn) {
  let ticking = false;
  return function(...args) {
    if (!ticking) {
      requestAnimationFrame(() => {
        fn.apply(this, args);
        ticking = false;
      });
      ticking = true;
    }
  };
}

// Usage: Smooth scroll handler
const handleScrollRaf = rafThrottle(() => {
  // This runs at most once per frame (60fps)
  updateParallax(); // Visual update synced to refresh!
});
window.addEventListener("scroll", handleScrollRaf);

// Cancel animation frame
const animationId = requestAnimationFrame(() => {
  drawCanvas();
});
cancelAnimationFrame(animationId); // Cancel if needed
๐Ÿ“‹ Scroll-based animations: Always use requestAnimationFrame. Scroll events fire many times per frame โ€” updating DOM on every scroll event causes layout thrashing. rAF batches visual updates to once per frame for smooth 60fps.
04๐Ÿ“ฆ Result Pattern โ€” Either Monad Style

Not every problem needs the same tool. Here is your decision matrix:

// The right tool for the right job

// โœ… SEARCH INPUT: Debounce (wait for user to stop typing)
searchInput.addEventListener("input", debounce((e) => {
  searchAPI(e.target.value); // One call after user pauses
}, 300));

// โœ… SCROLL HANDLER: Throttle or rAF (fire while scrolling)
window.addEventListener("scroll", throttle(() => {
  updateLazyImages(); // Regular updates during scroll
}, 100));

// โœ… ANIMATION: requestAnimationFrame (sync to refresh)
function animateElement() {
  element.style.transform = "translateX(" + x + "px)";
  if (x < 500) requestAnimationFrame(animateElement);
}

// โœ… RESIZE HANDLER: Debounce (wait until resizing stops)
window.addEventListener("resize", debounce(() => {
  recalculateLayout(); // One recalculation after resize ends
}, 250));

// โœ… BUTTON SPAM: Throttle (prevent rapid clicks)
buyButton.addEventListener("click", throttle(() => {
  processPayment(); // At most once per second
}, 1000));

// โŒ WRONG: Debounce on scroll โ€” handler never fires during scroll!
// โŒ WRONG: Throttle on search โ€” fires multiple API calls while typing!

Common mistake: Using debounce for scroll โ€” the user keeps scrolling, so the handler NEVER fires until they stop! Images never load, parallax never updates.

Common mistake: Using throttle for search โ€” it fires multiple unnecessary API calls while the user is still typing, wasting bandwidth and creating race conditions.

05๐Ÿงน Practical Error Handling Patterns

Leading edge debounce: Execute immediately on first call, then debounce subsequent calls. Good for buttons where you want instant feedback.

// Leading edge debounce (instant + then wait)
function leadingDebounce(fn, delay) {
  let timer;
  let called = false;
  return function(...args) {
    if (!called) {
      fn.apply(this, args); // Execute immediately on first call!
      called = true;
    }
    clearTimeout(timer);
    timer = setTimeout(() => called = false, delay);
  };
}

Cleanup pattern for SPAs: Always cancel pending timers and remove event listeners when components unmount โ€” prevents memory leaks and stale updates.

class ScrollHandler {
  constructor() {
    this.handleScroll = throttle(this.onScroll.bind(this), 100);
    window.addEventListener("scroll", this.handleScroll);
  }
  
  onScroll() {
    updateLazyImages();
  }
  
  destroy() {
    window.removeEventListener("scroll", this.handleScroll);
    // If using debounce with .cancel():
    // this.handleScroll.cancel();
  }
}

Debounce returning a Promise: Return a Promise that resolves when the debounced function finally executes โ€” useful for async workflows.

function debouncePromise(fn, delay) {
  let timer;
  let pendingResolve;
  return function(...args) {
    clearTimeout(timer);
    return new Promise(resolve => {
      pendingResolve = resolve;
      timer = setTimeout(() => {
        const result = fn.apply(this, args);
        pendingResolve(result);
      }, delay);
    });
  };
}

The this context: Debounce and throttle wrappers must preserve this โ€” use function() not arrow functions for the wrapper, so fn.apply(this, args) works correctly.

Lo kar liya โ€” Key Points:

  • โœ… Debounce waits until the user STOPS calling before executing โ€” use for search input, auto-save, resize
  • โœ… Throttle executes at most once every X ms โ€” use for scroll, mouse move, click spam prevention
  • โœ… requestAnimationFrame syncs callbacks to the display refresh rate (60fps) โ€” use for animations and visual updates
  • โœ… rAF is automatically paused when the tab is hidden, saving CPU and battery
  • โœ… Use debounce for search (wait for pause), throttle for scroll (fire regularly), rAF for animations (sync to screen)
  • โœ… Debounce on scroll is wrong โ€” handler never fires during scroll. Throttle on search is wrong โ€” fires too many API calls
  • โœ… Always cancel timers and remove event listeners when components unmount to prevent memory leaks
Course Search
Search across all chapters & stages
๐Ÿ“–

Search the course

Type any topic โ€” branching, stash, rebase, hooks โ€” and jump straight to that chapter.

merge branchesgit stashundo commitrebase