| |

JavaScript 66 🧬 Performance — debounce/throttle

Some events fire hundreds of times per second. Scroll, resize, mousemove, input — the browser doesn’t wait for the user to pause; it fires a callback on every change. If your handler does real work, that’s hundreds of times more work than necessary.

Debounce and throttle are the two patterns that fix this. They both limit how often a function runs, but they do it in different ways, for different scenarios. Knowing which to reach for is the difference between a smooth UI and a janky one.

Key point: Debounce waits for silence. Throttle runs at a fixed rate. If you need “run after the user stops”, use debounce. If you need “run every N milliseconds while the user acts”, use throttle.


a – Why frequency control matters

Every event handler runs on the main thread. If a handler takes 10ms, and scroll fires 100 times per second, that’s 1000ms of work per second — the browser is doing nothing but running your code. Frames drop, input lags, and the page feels stuck.

The three problem events:

  • scroll — fires on every pixel change
  • resize — fires on every pixel of window resize
  • input / keydown — fires on every keystroke
  • mousemove — fires dozens of times per second
  • touchmove — same on mobile

Without control:

window.addEventListener('scroll', () => {
  const progress = window.scrollY / document.body.scrollHeight;
  updateProgressBar(progress);
});

Even a simple calculation runs hundreds of times per scroll. Add DOM reads, network requests, or layout calculations, and the page stutters.

The cost of a handler isn’t the code — it’s the DOM work. Reading layout (offsetHeight, getBoundingClientRect) forces the browser to recalculate. Writing to the DOM forces a re-render. Running these on every event is what kills performance.

The two answers:

PatternWhen it fires
DebounceAfter events stop for N ms
ThrottleAt most once per N ms

They solve different problems. Use the wrong one and you’ll either delay actions unnecessarily or still fire too often.

A working definition:

  • Debounce — collapse a burst of calls into a single call, after the burst ends
  • Throttle — guarantee a max rate, spreading calls evenly

b – Debounce

Debounce waits until the event stops firing for a given delay, then runs the handler once. If more events arrive before the delay elapses, the timer resets.

Basic implementation:

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

Every call clears the previous timer and sets a new one. Only the last call in a burst actually triggers the function — after delay ms of silence.

Why it works: Each call cancels the previous. If calls keep coming faster than delay, the timer keeps resetting, and the function never fires until the stream stops.

The search-as-you-type pattern:

const search = debounce((query) => {
  fetch(`/api/search?q=${encodeURIComponent(query)}`);
}, 300);

input.addEventListener('input', (event) => {
  search(event.target.value);
});

The user types hello. Without debounce, six requests fire — one per letter. With debounce, the user types, pauses, and one request fires 300ms after the last keystroke. This is the classic debounce use case.

The resize pattern:

const onResize = debounce(() => {
  recalculateLayout();
}, 200);

window.addEventListener('resize', onResize);

The user drags the window edge. Layout recalculates once — after they stop. Without debounce, it recalculates continuously, and the drag becomes laggy.

The auto-save pattern:

const save = debounce((content) => {
  localStorage.setItem('draft', content);
}, 500);

editor.addEventListener('input', (e) => save(e.target.value));

The draft saves 500ms after the user stops typing — frequent enough to protect work, infrequent enough not to hammer storage.

Leading vs trailing debounce:

The default is trailing — the function runs after the delay. Some cases need leading — the function runs immediately, then doesn’t run again until the burst ends.

function debounce(fn, delay, { leading = false } = {}) {
  let timer;
  return (...args) => {
    const callNow = leading && !timer;
    clearTimeout(timer);
    timer = setTimeout(() => {
      timer = null;
      if (!leading) fn(...args);
    }, delay);
    if (callNow) fn(...args);
  };
}
  • Leading — fire on the first call, ignore the rest until quiet
  • Trailing — wait, then fire on the last call
  • Both — fire immediately and again at the end

Leading is useful for button clicks where you want an instant response but don’t want repeated clicks during a burst.

Debounce with immediate return:

function debounce(fn, delay) {
  let timer;
  return (...args) => {
    if (timer) clearTimeout(timer);
    else fn(...args);   // first call runs immediately
    timer = setTimeout(() => (timer = null), delay);
  };
}

This is the leading form — first call fires, subsequent calls within delay are ignored.

What debounce does NOT do:

  • It doesn’t run during the burst — the user sees nothing until they pause
  • If the user never pauses, it never runs
  • It doesn’t guarantee a maximum delay

For live feedback (progress bars, autocomplete) you might want a throttle alongside, or use a max-wait variant.

Max-wait debounce:

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

    if (!maxTimer) {
      maxTimer = setTimeout(() => {
        clearTimeout(timer);
        maxTimer = null;
        fn(...args);
      }, maxWait);
    }
  };
}

Guarantees the function runs at least every maxWait ms, even if events keep firing. Useful for long-running interactions.

Where debounce shines:

Use caseWhy debounce
Search inputWait for typing to stop
Auto-saveSave when editing pauses
Form validationValidate after user pauses
Resize recalculationRecalculate once after drag
API calls on inputPrevent request storms
localStorage writesAvoid write amplification

c – Throttle

Throttle runs the handler at most once every N milliseconds, no matter how often the event fires. Where debounce waits for silence, throttle runs on a schedule — at a fixed rate during the burst.

Basic implementation:

function throttle(fn, limit) {
  let waiting = false;
  return (...args) => {
    if (waiting) return;
    fn(...args);
    waiting = true;
    setTimeout(() => (waiting = false), limit);
  };
}

The first call runs immediately. Every call during the limit window is ignored. After the window, the next call runs.

A version that fires on the trailing edge:

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

This version guarantees the last call runs — so no event is dropped entirely. The simpler version may miss the last event if it falls inside a throttle window.

The scroll-progress pattern:

const updateProgress = throttle(() => {
  const progress = window.scrollY / (document.body.scrollHeight - window.innerHeight);
  progressBar.style.width = `${progress * 100}%`;
}, 100);

window.addEventListener('scroll', updateProgress);

The progress bar updates 10 times per second — smooth enough for humans, cheap enough for the browser.

The mousemove pattern:

const updateCursor = throttle((event) => {
  tooltip.style.left = `${event.clientX}px`;
  tooltip.style.top = `${event.clientY}px`;
}, 16);   // ~60fps

document.addEventListener('mousemove', updateCursor);

A tooltip that follows the cursor at 60fps doesn’t need to run at 200+ events per second. Throttling to ~16ms matches the display refresh rate.

The touchmove pattern:

const onTouchMove = throttle((event) => {
  handleDrag(event.touches[0]);
}, 50);

element.addEventListener('touchmove', onTouchMove);

Mobile touch events fire at the device’s sampling rate. Throttling to 20Hz keeps the UI responsive without burning battery.

Throttle with requestAnimationFrame:

For anything visual — animations, position updates, drawing — requestAnimationFrame is the right throttle. It fires at the display’s refresh rate, and it pauses when the tab is hidden.

let scheduled = false;

function onScroll() {
  if (scheduled) return;
  scheduled = true;

  requestAnimationFrame(() => {
    updateScrollIndicator();
    scheduled = false;
  });
}

window.addEventListener('scroll', onScroll);

This is the “rAF throttle” — naturally syncs with the browser’s paint cycle. Use it whenever you’re updating visuals on scroll or resize.

Why rAF beats a fixed throttle for visuals: The browser paints at 60Hz (or 120Hz on some devices). If your throttle fires at 100Hz, half the work is wasted — the frame hasn’t changed. rAF guarantees one update per frame, no more, no less.

Throttle vs debounce:

AspectDebounceThrottle
Fires whenAfter quietDuring burst, on schedule
FrequencyOnceAt most N per interval
Delays first call?Yes (trailing)No
Misses last event?NoSometimes (basic version)
Live feedback
Final state mattersOnly if trailing

Choosing between them:

  • Search input → debounce (don’t request until they pause)
  • Auto-save → debounce (save when typing stops)
  • Scroll progress bar → throttle (keep updating during scroll)
  • Mousemove tooltip → throttle (or rAF)
  • Window resize layout → debounce (recalculate once)
  • Game loop / animation → rAF
  • Button rapid clicks → throttle or debounce with leading
  • API polling → throttle (fixed interval)
  • Form validation → debounce (validate when user pauses)
  • Live search suggestions → throttle or debounce+throttle combined

Combining both:

Some cases need both — a maximum wait (throttle) plus quiet time (debounce). This is the “debounce with maxWait” pattern:

function smartDebounce(fn, delay, maxWait) {
  let timer, maxTimer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      clearTimeout(maxTimer);
      maxTimer = null;
      fn(...args);
    }, delay);

    if (!maxTimer) {
      maxTimer = setTimeout(() => {
        clearTimeout(timer);
        maxTimer = null;
        fn(...args);
      }, maxWait);
    }
  };
}

The function runs after delay ms of quiet, or after maxWait ms regardless — whichever comes first.

Common mistakes:

  • Using debounce for scroll progress — the bar would freeze until scrolling stops
  • Using throttle for search input — sends requests while typing
  • Forgetting to clear timers on cleanup
  • Not handling the this context — arrow functions capture it
  • Not preserving arguments — the wrapper must forward them

The cleanup problem:

A debounce or throttle that uses setTimeout must be cancelled when the component is destroyed. Otherwise the timer keeps running, and any closure it holds stays alive.

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

const save = debounce(saveFn, 500);

// Later
save.cancel();

Adding a .cancel() method is standard — lodash’s debounce and throttle both provide it.


Complete Example Session

// ============================================
// PART 1: BASIC DEBOUNCE
// ============================================

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

const log = debounce(msg => console.log(msg), 300);
log('a');
log('b');
log('c');
// Only 'c' logs, 300ms later
// [ c ]

// ============================================
// PART 2: DEBOUNCE SEARCH
// ============================================

const search = debounce(async (query) => {
  const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
  return res.json();
}, 300);

input.addEventListener('input', e => search(e.target.value));
// One request per pause, not one per keystroke

// ============================================
// PART 3: DEBOUNCE RESIZE
// ============================================

const onResize = debounce(() => {
  recalculateLayout();
}, 200);

window.addEventListener('resize', onResize);
// Recalculates once after the user stops dragging

// ============================================
// PART 4: LEADING DEBOUNCE
// ============================================

function debounceLeading(fn, delay) {
  let timer;
  return (...args) => {
    if (timer) return;
    fn(...args);
    timer = setTimeout(() => (timer = null), delay);
  };
}

const logLeading = debounceLeading(msg => console.log(msg), 300);
logLeading('a');
logLeading('b');
logLeading('c');
// [ a ]  ← fires immediately, others ignored

// ============================================
// PART 5: BASIC THROTTLE
// ============================================

function throttle(fn, limit) {
  let waiting = false;
  return (...args) => {
    if (waiting) return;
    fn(...args);
    waiting = true;
    setTimeout(() => (waiting = false), limit);
  };
}

const logThrottled = throttle(msg => console.log(msg), 300);
logThrottled('a');  // [ a ]
logThrottled('b');  // ignored
logThrottled('c');  // ignored
// Only 'a' logs in the first 300ms

// ============================================
// PART 6: THROTTLE WITH TRAILING
// ============================================

function throttleTrailing(fn, limit) {
  let last = 0;
  let timer;
  return (...args) => {
    const now = Date.now();
    if (now - last >= limit) {
      last = now;
      fn(...args);
    } else {
      clearTimeout(timer);
      timer = setTimeout(() => {
        last = Date.now();
        fn(...args);
      }, limit - (now - last));
    }
  };
}

// ============================================
// PART 7: SCROLL PROGRESS
// ============================================

const updateProgress = throttle(() => {
  const pct = window.scrollY / (document.body.scrollHeight - window.innerHeight);
  progressBar.style.width = `${pct * 100}%`;
}, 100);

window.addEventListener('scroll', updateProgress);
// Updates 10x per second during scroll

// ============================================
// PART 8: MOUSEMOVE TOOLTIP
// ============================================

const followCursor = throttle((e) => {
  tooltip.style.left = `${e.clientX}px`;
  tooltip.style.top = `${e.clientY}px`;
}, 16);

document.addEventListener('mousemove', followCursor);
// ~60fps

// ============================================
// PART 9: REQUESTANIMATIONFRAME THROTTLE
// ============================================

let scheduled = false;

function onScroll() {
  if (scheduled) return;
  scheduled = true;
  requestAnimationFrame(() => {
    updateScrollIndicator();
    scheduled = false;
  });
}

window.addEventListener('scroll', onScroll);
// One update per frame

// ============================================
// PART 10: DEBOUNCE WITH CANCEL
// ============================================

function debounceCancel(fn, delay) {
  let timer;
  const wrapped = (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
  wrapped.cancel = () => clearTimeout(timer);
  return wrapped;
}

const save = debounceCancel(saveFn, 500);
save('content');

// Later
save.cancel();

// ============================================
// PART 11: AUTO-SAVE
// ============================================

const autoSave = debounce((content) => {
  localStorage.setItem('draft', content);
  console.log('saved');
}, 500);

editor.addEventListener('input', e => autoSave(e.target.value));
// Saves 500ms after the user stops typing

// ============================================
// PART 12: DEBOUNCE WITH MAX WAIT
// ============================================

function debounceMaxWait(fn, delay, maxWait) {
  let timer, maxTimer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      clearTimeout(maxTimer);
      maxTimer = null;
      fn(...args);
    }, delay);

    if (!maxTimer) {
      maxTimer = setTimeout(() => {
        clearTimeout(timer);
        maxTimer = null;
        fn(...args);
      }, maxWait);
    }
  };
}

const smart = debounceMaxWait(doWork, 500, 2000);
// Runs after 500ms quiet, or every 2s max

// ============================================
// PART 13: BUTTON RAPID CLICKS
// ============================================

const submit = debounceLeading(async () => {
  await postData();
}, 1000);

submitBtn.addEventListener('click', submit);
// Fires immediately, ignores clicks for 1s

// ============================================
// PART 14: TOUCHMOVE THROTTLE
// ============================================

const onDrag = throttle((e) => {
  handleDrag(e.touches[0]);
}, 50);

element.addEventListener('touchmove', onDrag);
// 20Hz — smooth enough for drag, cheap on battery

// ============================================
// PART 15: RESIZE WITH DEBOUNCE + CANCEL
// ============================================

const onResize = debounceCancel(() => {
  recalculateLayout();
}, 200);

window.addEventListener('resize', onResize);

// Cleanup on unmount
window.removeEventListener('resize', onResize);
onResize.cancel();

// ============================================
// PART 16: FORM VALIDATION
// ============================================

const validate = debounce((value) => {
  if (value.length < 3) showError('Too short');
  else hideError();
}, 300);

input.addEventListener('input', e => validate(e.target.value));
// Validates 300ms after the user pauses

Quick Reference

Debounce

FeatureBehavior
FiresAfter quiet
DelayReset on each call
Live feedback
Trailing default
Leading variantOptional
Cancel methodShould provide

Throttle

FeatureBehavior
FiresAt most N per interval
First callImmediate
Live feedback
Trailing variantOptional
rAF variantFor visuals

When to Use

ScenarioPattern
Search inputDebounce
Auto-saveDebounce
Form validationDebounce
Resize layoutDebounce
Scroll progressThrottle
Mousemove tooltipThrottle
Touch dragThrottle
AnimationrAF
Button clicksLeading debounce
API pollingThrottle
Combined quiet + maxDebounce maxWait

Common Delays

UseDelay
Search input300ms
Auto-save500ms
Resize200ms
Scroll progress100ms
Mouse tooltip16ms
Touch drag50ms
Button1000ms

Debounce vs Throttle

AspectDebounceThrottle
Fires during burst
Fires after burstTrailing only
Cancels pending✅ (resets)
Guaranteed rate
Guaranteed finalTrailing only
Live updates

Variants

VariantBehavior
Trailing debounceRuns after quiet
Leading debounceRuns immediately
BothRuns immediately + at end
Max-wait debounceGuaranteed max delay
Basic throttleLeading edge only
Trailing throttleGuaranteed last call
rAF throttleSynced to frames

Best Practices

Do This:

// Use debounce for search
input.addEventListener('input', debounce(search, 300));   // ✅

// Use throttle for scroll
window.addEventListener('scroll', throttle(update, 100)); // ✅

// Use rAF for visuals
requestAnimationFrame(updatePosition);                    // ✅

// Provide a cancel method
debounce.cancel();                                        // ✅

// Match delay to UX
500ms for auto-save, 16ms for cursor                     // ✅

// Clean up on unmount
onResize.cancel();
window.removeEventListener('resize', onResize);          // ✅

// Combine max-wait for long interactions
debounceWithMaxWait(fn, 300, 2000);                       // ✅

Don’t Do This:

// Don't debounce scroll progress
window.addEventListener('scroll', debounce(update, 100)); // ❌ freezes

// Don't throttle search input
input.addEventListener('input', throttle(search, 300));   // ❌ spam

// Don't forget to cancel
const fn = debounce(save, 500);                            // ⚠️  leaks

// Don't use debounce for real-time feedback
mousemove → debounce(followCursor, 16)                    // ❌ lags

// Don't write your own rAF loop when rAF works
setInterval(updatePosition, 16);                          // ⚠️  out of sync

// Don't use timers for animation
setTimeout(animate, 16);                                  // ⚠️  use rAF

// Don't forget to forward arguments
debounce((...args) => fn(...args));                       // ✅
debounce(() => fn());                                     // ⚠️  args lost

Common Pitfalls

PitfallProblemSolution
Debounce on scrollFreezes until stopThrottle
Throttle on inputFires mid-typingDebounce
Missing cancelTimer leaksAdd .cancel()
Lost thisMethod breaksArrow / bind
Lost argumentsWrong args passedForward ...args
rAF not cancelledRuns after unmountCancel handle
Delay too shortWork still too frequentIncrease delay
Delay too longFeels laggyDecrease delay
No trailing throttleLast event droppedUse trailing version

Real-World Examples

1. Debounced search

const search = debounce(q => fetch(`/api?q=${q}`), 300);
input.addEventListener('input', e => search(e.target.value));

One request per pause, not per keystroke.

2. Debounced auto-save

const save = debounce(content => {
  localStorage.setItem('draft', content);
}, 500);

editor.addEventListener('input', e => save(e.target.value));

Saves half a second after typing stops.

3. Debounced validation

const validate = debounce(value => {
  showError(validateField(value));
}, 300);

input.addEventListener('input', e => validate(e.target.value));

Validation runs once the user pauses, not on every character.

4. Debounced resize

const onResize = debounce(() => recalcLayout(), 200);
window.addEventListener('resize', onResize);

One recalculation after the user stops dragging.

5. Throttled scroll progress

const update = throttle(() => {
  progress.style.width = scrollPercent() + '%';
}, 100);

window.addEventListener('scroll', update);

Progress bar updates 10× per second.

6. Throttled mousemove

const follow = throttle(e => {
  tooltip.style.transform = `translate(${e.clientX}px, ${e.clientY}px)`;
}, 16);

document.addEventListener('mousemove', follow);

Smooth cursor tracking at 60fps.

7. rAF scroll indicator

let scheduled = false;

function onScroll() {
  if (scheduled) return;
  scheduled = true;
  requestAnimationFrame(() => {
    updateIndicator();
    scheduled = false;
  });
}

window.addEventListener('scroll', onScroll);

Updates exactly once per frame.

8. Leading debounce for clicks

const submit = debounceLeading(() => postData(), 1000);
button.addEventListener('click', submit);

Instant response, ignores rapid re-clicks.

9. Debounce with cancel

const save = debounce(saveFn, 500);
save('content');

// Cleanup
save.cancel();

Stops the pending call when the component unmounts.

10. Combined debounce + max wait

const smart = debounceMaxWait(sync, 300, 2000);

Waits for quiet, but guarantees a sync every 2 seconds during long bursts.


Visual: Debounce vs Throttle

┌──────────────────────────────────────────────┐
│  Events:  ▲▲▲▲▲▲▲▲▲▲                         │
│           ││││││││││                         │
│  Time:    ─────────────────────────────►     │
│                                              │
│  Debounce:                    ◆              │
│                              fires once      │
│                              after quiet     │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Events:  ▲▲▲▲▲▲▲▲▲▲                         │
│           ││││││││││                         │
│  Time:    ─────────────────────────────►     │
│                                              │
│  Throttle: ◆    ◆    ◆    ◆                  │
│            fires at fixed intervals          │
│                                              │
└──────────────────────────────────────────────┘

Visual: Debounce Timer Reset

┌──────────────────────────────────────────────┐
│  Call 1 ───► timer(300ms)                    │
│  Call 2 ───► cancel, restart timer           │
│  Call 3 ───► cancel, restart timer           │
│  Call 4 ───► cancel, restart timer           │
│  ...no more calls                            │
│  300ms later ───► function runs              │
│                                              │
│  Every call resets the countdown             │
│                                              │
└──────────────────────────────────────────────┘

Visual: Throttle Window

┌──────────────────────────────────────────────┐
│  Call 1  ───► runs immediately               │
│           ┌──── window (300ms) ────┐         │
│  Call 2  │ ignored                  │         │
│  Call 3  │ ignored                  │         │
│  Call 4  │ ignored                  │         │
│           └────────────────────────┘         │
│  Call 5  ───► runs (window expired)          │
│           ┌──── window ─────────────┐        │
│  Call 6  │ ignored                  │        │
│                                              │
└──────────────────────────────────────────────┘

Visual: Leading vs Trailing

┌──────────────────────────────────────────────┐
│  Trailing debounce (default)                 │
│                                              │
│  ▲▲▲▲▲▲▲  ──[300ms]──►  ◆                    │
│  Nothing until quiet, then one call          │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Leading debounce                            │
│                                              │
│  ◆  ▲▲▲▲▲▲  ──[300ms]──►  (nothing)          │
│  Immediate call, rest ignored                │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Both                                        │
│                                              │
│  ◆  ▲▲▲▲▲▲  ──[300ms]──►  ◆                  │
│  Immediate + trailing                        │
│                                              │
└──────────────────────────────────────────────┘

Summary

ConceptSyntaxPurpose
Debouncedebounce(fn, delay)Run after quiet
Throttlethrottle(fn, limit)Run at fixed rate
LeadingImmediate first callDebounce variant
TrailingRun after last eventDefault
Cancel.cancel()Stop pending
rAF throttlerequestAnimationFrameSync to frames
Max waitdebounce(fn, d, max)Guarantee upper bound

Key takeaways:

  • Debounce collapses a burst into one call after the events stop
  • Throttle runs at most once per interval during the burst
  • Search input, auto-save, validation, resize → debounce
  • Scroll, mousemove, touchmove, animation → throttle or rAF
  • Leading debounce fires immediately and ignores the rest — good for buttons
  • Trailing throttle guarantees the last event runs — good when the final state matters
  • requestAnimationFrame is the right throttle for anything visual
  • Always provide a .cancel() so cleanup can stop pending timers
  • Match the delay to UX — 300ms for search, 16ms for cursor, 500ms for auto-save
  • Combine debounce with max-wait when you need both quiet and a ceiling
  • Forward arguments and preserve this in your wrappers
  • Clean up on component unmount to avoid leaks

Remember: Debounce and throttle are the two tools for taming high-frequency events. Debounce is for “do it once when they’re done.” Throttle is for “keep doing it, but not too often.” Use the wrong one and you’ll either freeze the UI or flood it. Use requestAnimationFrame for anything that touches the screen. And always cancel pending timers when the component goes away. Master these two patterns, and your UI stays smooth under any event storm.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!