Debounce waits until calls stop for a set time, then runs once, which suits search boxes and autosave. Throttle runs at most once per interval while calls keep coming, which suits scroll, resize and pointer tracking. Both take a function and a delay in milliseconds and return a wrapped function you call instead.
| Situation | Use | Why |
|---|---|---|
| Search as you type | Debounce, about 300 ms | One request after the person pauses |
| Autosave a form | Debounce, about 1000 ms | Save once typing settles |
| Scroll position or progress bar | Throttle, about 100 ms | Regular updates while scrolling |
| Window resize layout maths | Throttle or debounce | Throttle for live feedback, debounce for the final size |
| Prevent double submit clicks | Debounce with leading edge | First click counts, repeats are ignored |
Updated
The two words get mixed up because both cut down how often a function runs. The pad below shows the difference on a shared timeline: every raw event as a thin tick, every debounced call and every throttled call as a dot. Change the delay and the pattern changes with it.
Move over the pad. Thin ticks are raw events; dots are calls that actually ran after debouncing or throttling.
Trailing edge. cancel() drops any pending call.
export function debounce(fn, wait = 300) {
let timer;
function debounced(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), wait);
}
debounced.cancel = () => clearTimeout(timer);
return debounced;
}
const search = debounce((q) => fetch(`/api/search?q=${encodeURIComponent(q)}`), 300);
input.addEventListener("input", (e) => search(e.target.value));Runs on the leading edge, then once more at the end of the window if calls arrived meanwhile, so the last value is never lost.
export function throttle(fn, wait = 100) {
let last = 0;
let timer;
let pendingArgs;
return function throttled(...args) {
const now = Date.now();
const remaining = wait - (now - last);
if (remaining <= 0) {
clearTimeout(timer);
timer = undefined;
last = now;
fn.apply(this, args);
} else {
pendingArgs = args;
if (!timer) {
timer = setTimeout(() => {
last = Date.now();
timer = undefined;
fn.apply(this, pendingArgs);
}, remaining);
}
}
};
}
window.addEventListener("scroll", throttle(() => updateProgress(window.scrollY), 100), { passive: true });Keeps one timer for the component's life, always calls the latest callback, and clears the timer on unmount.
import { useCallback, useEffect, useRef } from "react";
export function useDebouncedCallback<A extends unknown[]>(fn: (...args: A) => void, wait = 300) {
const fnRef = useRef(fn);
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
useEffect(() => { fnRef.current = fn; }, [fn]);
useEffect(() => () => clearTimeout(timer.current), []);
return useCallback((...args: A) => {
clearTimeout(timer.current);
timer.current = setTimeout(() => fnRef.current(...args), wait);
}, [wait]);
}Returns a copy of a fast-changing value that updates at most once per interval.
import { useEffect, useRef, useState } from "react";
export function useThrottledValue<T>(value: T, wait = 100): T {
const [throttled, setThrottled] = useState(value);
const last = useRef(0);
useEffect(() => {
const remaining = wait - (Date.now() - last.current);
if (remaining <= 0) {
last.current = Date.now();
setThrottled(value);
return;
}
const id = setTimeout(() => {
last.current = Date.now();
setThrottled(value);
}, remaining);
return () => clearTimeout(id);
}, [value, wait]);
return throttled;
}
Each call clears the pending timer and starts a new one. Only when the timer survives the full delay does the original function run, with the arguments from the last call.
That makes debounce perfect for intent: you do not care about each keystroke, only about the word someone finished typing. The cost is latency, since nothing happens until the pause.
A throttle remembers when it last ran. A call inside the interval is dropped or, in the trailing version, saved to run when the interval ends. Calls outside the interval run at once.
Use it when the user should see continuous feedback, such as a sticky header reacting to scroll, but running on every event would be wasteful.
Debounce restarts its timer on every call and only runs once the calls pause. Throttle ignores calls inside the current window but runs again as soon as the window passes, so it fires steadily during a long burst.
The leading edge is the first call of a burst; the trailing edge is the moment the burst's interval ends. A trailing debounce runs after the pause, a leading one runs immediately and then ignores calls until the pause. Throttles can fire on either edge or both.
No. The functions on this page are a few lines each. A library version adds options such as maxWait, cancel and flush, which are handy but easy to add yourself if you need only one of them.
There is no single right number. Start around 300 milliseconds for typing and 100 for scroll or pointer work, then adjust by feel. Shorter feels more responsive but saves less work.
Related: JavaScript event listeners, Infinite scroll in React, Sticky header, JavaScript async fetch, React performance APIs.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.