Skip to content
Commitpedia
Log in Join & edit

Debounce and throttle functions in plain JavaScript

Two small dependency-free helpers that limit how often a function runs: debounce waits for a pause, throttle runs at most once per interval.

Drafted with AI from official documentation. Scripts were run in a test environment and repository links verified. Awaiting a human check. Did it work for you? Last edited Outdated?

Edit history of Debounce and throttle functions in plain JavaScript: 1 recorded edit
Edit history · 1 edit
Type
Script
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Language
javascript
Licence
CC0-1.0

What this changes

Read-only: run with Node it only prints a timing demo to the terminal; nothing on your system is changed. In a page it only delays or limits calls to your own functions.

How to undo it

Nothing to undo.

Some events fire dozens of times per second: typing, scrolling, resizing, mouse moves. Running expensive work on every event wastes CPU and network. Debounce and throttle are the two standard ways to slow it down.

Which one do I need?

  • Debounce: run once, after the events stop for a given time. Use it for search-as-you-type, autosave, and validating a field after the user finishes typing.
  • Throttle: run at most once per interval while events keep coming. Use it for scroll position tracking, resize handling, and progress reporting.

Imagine an event every 50 ms for half a second. A 200 ms debounce calls your function once, about 200 ms after the last event. A 200 ms throttle calls it immediately, then roughly every 200 ms, and once more at the end with the latest arguments.

The script

Save as timing.js. The functions work in browsers and Node. Running node timing.js prints a short demo that simulates rapid events.

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

function throttle(fn, interval = 300) {
  let last = 0;
  let timer;
  let pendingArgs;
  return function throttled(...args) {
    const now = Date.now();
    const remaining = interval - (now - last);
    if (remaining <= 0) {
      clearTimeout(timer);
      timer = undefined;
      last = now;
      fn.apply(this, args);
    } else {
      pendingArgs = args;
      timer ??= setTimeout(() => {
        last = Date.now();
        timer = undefined;
        fn.apply(this, pendingArgs);
      }, remaining);
    }
  };
}

if (typeof module !== "undefined" && require.main === module) {
  const start = Date.now();
  const log = (label) => (value) => console.log(`${label} ${value} at ~${Math.round((Date.now() - start) / 50) * 50} ms`);
  const search = debounce(log("debounced:"), 200);
  const scroll = throttle(log("throttled:"), 200);
  let n = 0;
  const id = setInterval(() => {
    n++;
    search(`q${n}`);
    scroll(`y${n}`);
    if (n === 10) clearInterval(id);
  }, 50);
}

if (typeof module !== "undefined") module.exports = { debounce, throttle };

Using it in a browser

const input = document.querySelector("#search");
const runSearch = debounce(async (query) => {
  const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
  renderResults(await res.json());
}, 300);

input.addEventListener("input", (e) => runSearch(e.target.value));

window.addEventListener("scroll", throttle(() => {
  saveScrollPosition(window.scrollY);
}, 200), { passive: true });

How they work

debounce keeps a single timer. Every call cancels the previous timer and starts a new one, so the function only runs when the timer survives a full quiet period. cancel() lets you drop a pending call, for example when a component is removed.

throttle remembers when it last ran. If enough time has passed, it runs right away (the leading call). Otherwise it stores the newest arguments and schedules one trailing call for when the interval ends, so the final state is never lost.

Gotchas

  • Create the debounced function once and reuse it. Calling debounce(fn) inside an event handler creates a fresh timer each time and debounces nothing. In React, keep it in useMemo or useRef.
  • Both helpers forward this, so they work as object methods, but arrow functions passed in keep their own this.
  • A debounced async function can still overlap with a slow earlier request. For search boxes, also cancel the previous request with an AbortController or ignore stale responses.
  • For animation tied to the screen refresh, requestAnimationFrame is usually a better throttle than a fixed interval.
  • If you already use Lodash, its _.debounce and _.throttle add options like maxWait and leading/trailing switches.

Did this work for you?

No votes yet. Be the first.

If it did not work, the report form below tells the editors what went wrong.

Repos in the wild

  • Lodash lodash/lodash A JavaScript utility library with helpers for arrays, objects, strings and functions, including debounce, throttle, deep cloning and safe property access. GitHub

History

Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.

  1. Dominik +4,010 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?