Rapid-fire events — keystrokes, scrolls, resizes — can overwhelm your handlers. Debouncing and throttling both tame them, but they behave differently and are easy to mix up.
The difference in one sentence
- Debounce: wait until the events stop, then run once.
- Throttle: run at most once per interval while events keep firing.
Debounce
Great for "wait until the user is done typing" — search-as-you-type, autosave, validating a form field.
export function debounce<A extends unknown[]>(
fn: (...args: A) => void,
delay = 300,
) {
let timer: ReturnType<typeof setTimeout> | undefined
return (...args: A) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), delay)
}
}
// Usage
const search = debounce((q: string) => fetchResults(q), 250)
input.addEventListener("input", (e) => search(e.target.value))Throttle
Great for "run periodically during a continuous action" — scroll position, mouse tracking, progress updates.
export function throttle<A extends unknown[]>(
fn: (...args: A) => void,
interval = 200,
) {
let last = 0
return (...args: A) => {
const now = Date.now()
if (now - last >= interval) {
last = now
fn(...args)
}
}
}Using it in React
Wrap the debounced function in a ref so it survives re-renders:
import { useEffect, useState } from "react"
export function useDebouncedValue<T>(value: T, delay = 300) {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(id)
}, [value, delay])
return debounced
}Which one do I want?
Ask: do I care about the final state, or a steady sample of intermediate states? If the final state matters, debounce. If you want smooth periodic updates, throttle. Get this right and janky UIs become buttery smooth.