Debounce vs Throttle, Visualised

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.

Which one to use
SituationUseWhy
Search as you typeDebounce, about 300 msOne request after the person pauses
Autosave a formDebounce, about 1000 msSave once typing settles
Scroll position or progress barThrottle, about 100 msRegular updates while scrolling
Window resize layout mathsThrottle or debounceThrottle for live feedback, debounce for the final size
Prevent double submit clicksDebounce with leading edgeFirst 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.

Visualiser

Move over the pad. Thin ticks are raw events; dots are calls that actually ran after debouncing or throttling.

Plain JavaScript versions

debounce(fn, wait)

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));

throttle(fn, wait)

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 });

React hooks

useDebouncedCallback

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]);
}

useThrottledValue

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;
}

How debounce works

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.

How throttle works

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.

How it works

  1. Move your pointer or finger across the pad, or hold a key down while it has focus.
  2. Raw events fill the top lane. The middle lane shows debounced calls, the bottom lane throttled calls.
  3. Drag the delay slider and repeat to see how the gap changes both patterns.
  4. Copy the plain functions or the React hooks below for your own code.

Tips

Frequently asked questions

What is the difference between debounce and throttle?

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.

What are leading and trailing edges?

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.

Do I need lodash for debounce?

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.

How long should the delay be?

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.

More in Guides

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.

Product

Who It's For

Features

Resources

Company

View Sitemap