Debounce and Throttle

Control how often code runs during fast events like typing, scrolling and resizing with debounce and throttle, and write your own reusable versions.

Lesson 40 of 56Intermediate16 min read

Some events fire a lot. Typing in a search box fires an input event for every key. Scrolling fires scroll dozens of times a second. Dragging the window edge fires resize constantly. If each event triggers expensive work, like a network request, a re-render or a layout calculation, the page stutters, batteries drain and servers get flooded with requests nobody needed.

Two classic techniques fix this by controlling how often your code runs. Debounce waits until the events stop for a moment, then runs once: perfect for search boxes, where you only care about what the user finally typed. Throttle runs at most once every so often while events keep coming: perfect for scroll positions and progress bars, where you want regular updates but not hundreds per second.

You met a simple debounced search in the timers lesson. In this lesson you'll understand exactly how each technique works, see them side by side, write reusable debounce and throttle functions using closures, and learn which to choose for common situations.

Debounce vs throttle

DebounceThrottle
RunsOnce, after events stop for a set timeAt most once per time window, while events continue
Like…A lift door that waits until people stop walking inA tap that drips at a steady rate
Best forSearch-as-you-type, autosave, validating a field, window resize finishedScroll position, infinite scroll checks, mouse move tracking, game input
With 10 keystrokes in 1 second, 300ms delayRuns once, 300ms after the last keyRuns about 4 times

Ask yourself: "Do I need the final value, or regular updates along the way?" Final value means debounce; regular updates mean throttle.

How debounce works

JavaScript
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

const search = debounce((text) => console.log("Search for", text), 300);
input.addEventListener("input", (e) => search(e.target.value));

Each call cancels the previous timer and starts a new one. Only when 300ms pass with no new calls does the timer survive and fn run. The variable timer lives in a closure, so every call to the returned function shares it.

How throttle works

JavaScript
function throttle(fn, interval) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last >= interval) {
      last = now;
      fn(...args);
    }
  };
}

window.addEventListener("scroll", throttle(() => console.log(scrollY), 200));

This version runs immediately on the first event, then ignores further calls until interval milliseconds have passed. Fuller versions also run once more at the end (a "trailing" call) so the final position is never missed.

Leading, trailing and alternatives

OptionMeaning
Trailing (debounce default)Run after the pause
LeadingRun on the first event, then ignore the rest until things calm down: good for "submit" buttons to stop double clicks
requestAnimationFrameThrottle visual updates to once per screen frame
{ passive: true }Lets the browser scroll without waiting for your listener
IntersectionObserver / matchMediaOften remove the need for scroll and resize listeners entirely

Libraries such as Lodash provide well-tested debounce and throttle with leading, trailing and maxWait options. Writing your own is a great exercise and a popular interview question.

JavaScript Debounce and Throttle Syntax

Debounce and throttle

const debounced = debounce(fn, 300);
input.addEventListener("input", (e) => debounced(e.target.value));

const throttled = throttle(fn, 200);
window.addEventListener("scroll", throttled, { passive: true });

let ticking = false;
addEventListener("scroll", () => {
  if (ticking) return;
  ticking = true;
  requestAnimationFrame(() => { update(); ticking = false; });
});

JavaScript Debounce and Throttle Summary

  • Fast events like input, scroll and resize can run expensive code far too often.
  • Debounce runs once after events stop for a set time: use it when you need the final value.
  • Throttle runs at most once per interval while events continue: use it for regular updates.
  • Both are built with closures that remember a timer or a timestamp.
  • Observers, matchMedia and requestAnimationFrame can replace many scroll and resize listeners.

JavaScript Debounce and Throttle Examples

Each example runs in the page. Edit the code and press Run to see what changes.

Example 1

Raw vs debounced vs throttled

Type quickly in the box and watch the three counters.

function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}
function throttle(fn, interval) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last >= interval) { last = now; fn(...args); }
  };
}

const counts = { raw: 0, deb: 0, thr: 0 };
const show = (id) => (document.getElementById(id).textContent = ++counts[id]);
const onDeb = debounce(() => show("deb"), 400);
const onThr = throttle(() => show("thr"), 400);

document.querySelector("#box").addEventListener("input", () => {
  show("raw");
  onDeb();
  onThr();
});
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Lines 1 to 7Debounce: every call resets the timer.
  2. Lines 8 to 14Throttle: calls are ignored until the interval has passed.
  3. Lines 18 to 19Two wrapped versions of the same counter.
  4. Lines 21 to 25Every keystroke goes to all three, but they run very different numbers of times.
Example 2

A debounced search with stale result protection

const fruit = ["apple", "apricot", "banana", "blackberry", "blueberry", "cherry", "grape", "mango"];

function fakeSearch(term) {
  return new Promise((resolve) => {
    const delay = 100 + Math.random() * 400;
    setTimeout(() => resolve(fruit.filter((f) => f.includes(term))), delay);
  });
}

let latest = 0;
let timer;
document.querySelector("#q").addEventListener("input", (e) => {
  clearTimeout(timer);
  timer = setTimeout(async () => {
    const id = ++latest;
    const term = e.target.value.trim().toLowerCase();
    const found = term ? await fakeSearch(term) : [];
    if (id !== latest) return;
    document.querySelector("#results").replaceChildren(
      ...found.map((f) => Object.assign(document.createElement("li"), { textContent: f }))
    );
    console.log(`Searched "${term}": ${found.length} results`);
  }, 300);
});
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Lines 3 to 8A pretend server that answers after a random delay.
  2. Lines 13 to 14Debounce: only search 300ms after typing stops.
  3. Line 15Number each request.
  4. Line 18If a newer search started meanwhile, ignore this older answer so results never flash back.
Example 3

Throttled scroll progress with requestAnimationFrame

Scroll inside the box.

const article = document.querySelector("#article");
const bar = document.querySelector("#bar");
let ticking = false;
let updates = 0;

article.addEventListener("scroll", () => {
  if (ticking) return;
  ticking = true;
  requestAnimationFrame(() => {
    const max = article.scrollHeight - article.clientHeight;
    bar.style.width = `${(article.scrollTop / max) * 100}%`;
    ticking = false;
    if (++updates % 10 === 0) console.log(`${updates} bar updates so far`);
  });
}, { passive: true });
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Lines 7 to 8If an update is already scheduled for this frame, skip.
  2. Line 9Do the work once, right before the next paint.
  3. Lines 10 to 11Width = how far through the article you are.
  4. Line 15passive lets scrolling stay smooth.

Common JavaScript Debounce and Throttle Mistakes

Creating a new debounced function on every event

Don't
input.addEventListener("input", () => debounce(search, 300)()); // a fresh timer each time, so nothing is debounced
Do
const debouncedSearch = debounce(search, 300);
input.addEventListener("input", () => debouncedSearch());

Debouncing a scroll position

Don't
window.addEventListener("scroll", debounce(updateProgressBar, 200)); // bar only moves after scrolling stops
Do
window.addEventListener("scroll", throttle(updateProgressBar, 100), { passive: true });

Losing arguments and this

Don't
function debounce(fn, ms) { let t; return () => { clearTimeout(t); t = setTimeout(fn, ms); }; } // drops arguments
Do
function debounce(fn, ms) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; }

JavaScript Debounce and Throttle in Real Projects

Where these are used

Google's search suggestions, address lookups and username availability checks debounce input before asking the server. Infinite scrolling feeds, sticky headers and reading progress bars throttle scroll handling. Editors like Google Docs debounce autosave, and analytics libraries throttle mouse-movement tracking. Lodash's debounce and throttle are among the most downloaded JavaScript utilities.

JavaScript Debounce and Throttle Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Cancel the previous timer so only the last call runs.

let timer;
function onType(text) {
  (timer);
  timer = setTimeout(() => console.log(text), 300);
}
onType("a"); onType("ab"); onType("abc");
Predict the outputmedium

log is debounced with 100ms. What is logged after a short wait?

const log = debounce((x) => console.log(x), 100);
log(1); log(2); log(3);
setTimeout(() => log(4), 300);
Multiple choiceeasy

Which technique suits a search box that queries a server?

Multiple choicemedium

Which technique suits updating a scroll progress bar?

Fix the bugmedium

debounce(fn, ms) should return a function that waits until it hasn't been called for ms milliseconds, then calls fn once with the latest arguments. Fix it.

function debounce(fn, ms) {
  return (...args) => {
    let timer;
    clearTimeout(timer);
    timer = setTimeout(fn, ms);
  };
}
const calls = [];
const d = debounce((v) => calls.push(v), 50);
d("a"); d("b"); d("c");
ConsoleEdit the code, then press Run or Ctrl+Enter
Checks
  • Runs once with the latest value
  • Separate bursts run separately

JavaScript Debounce and Throttle Quiz

5 questions to check what you learned.

  1. What does debounce do?

    • Runs code faster
    • Runs code at a fixed rate forever
    • Removes event listeners
    • Waits until events stop for a while, then runs once
  2. With a 1000ms throttle and constant events for 5 seconds, roughly how many times does the function run?

    • Once
    • Hundreds of times
    • About 5 times
    • Never
  3. Why do debounce and throttle need closures?

    • The returned function must remember the timer or last run time between calls
    • To make them asynchronous
    • Closures run faster
    • To access the DOM
  4. What does a leading debounce do?

    • Runs only after the pause
    • Runs on the first event, then ignores the rest until things calm down
    • Runs twice
    • Runs before the page loads
  5. Why can a debounced search still show wrong results?

    • Debounce changes the search term
    • Debounce is unreliable
    • Servers ignore debounced requests
    • An older, slower request can finish after a newer one, so results must be checked as the latest

JavaScript Debounce and Throttle Challenge

Write throttle with a trailing call

Write throttle(fn, interval) that returns a function which:

  1. Calls fn immediately on the first call
  2. Ignores further calls until interval ms have passed since the last run, but remembers the latest arguments
  3. When the interval ends, if there were ignored calls, runs fn once more with the latest arguments (a trailing call)
function throttle(fn, interval) {

}

const seen = [];
const t = throttle((v) => seen.push(v), 100);
t(1); t(2); t(3);
setTimeout(() => console.log(seen), 300);
ConsoleEdit the code, then press Run or Ctrl+Enter
Checks
  • First call runs immediately
  • Trailing call uses the latest arguments
  • No extra trailing call when quiet