Debounce & Throttle
Debounce delays execution until after N ms have passed since the last invocation — use for search input, resize handlers. Throttle limits execution to at most once every N ms — use for scroll handlers, rate-limited API calls.
Debounce = elevator doors: they keep resetting the close timer every time someone walks in. Throttle = a subway train: it leaves every 5 minutes no matter how many people are waiting on the platform.
Debounce implementation: store a timer ID; on each call, clear the previous timer and set a new one. The callback only fires if the timer completes without interruption. Leading-edge debounce fires immediately on first call, then ignores until quiet period — better UX for button clicks. Throttle implementation: track last execution time; only invoke if the elapsed time exceeds the interval. requestAnimationFrame throttle is a common pattern for scroll handlers: call rAF once per scroll event, update once per frame, cancel pending rAF before registering a new one.
Lodash's debounce has a sophisticated implementation with leading/trailing edge options, maxWait (ensures at least one call within a window), and cancellation. The core challenge: debounce/throttle must preserve this binding and arguments — modern implementations use rest parameters and closures. For React hooks, useCallback(debounce(...), []) is an anti-pattern because debounce creates a new closure each render, losing timer state — the correct approach is useRef to store the debounced function across renders or useMemo with an empty dep array. For TypeScript, preserving the function signature with generics is important: function debounce<T extends (...args: unknown[]) => unknown>(fn: T, ms: number): (...args: Parameters<T>) => void.
Debounce collapses multiple rapid calls into one, firing after the quiet period — ideal for input events where you only care about the final value. Throttle limits call rate to once per interval — ideal for scroll/resize where you want regular updates but can't handle every event. Both prevent performance bottlenecks by reducing work. Can you implement debounce? — store a timer, clearTimeout on each call, setTimeout for the callback, return the wrapper function.
In React, creating a debounced function inside a functional component without useRef/useMemo creates a new debounce closure every render, resetting the timer state. This means the debounce never actually delays — every re-render gives you a fresh debounce with no pending timer. Store debounced functions in useRef.