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 changeresize— fires on every pixel of window resizeinput/keydown— fires on every keystrokemousemove— fires dozens of times per secondtouchmove— 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:
| Pattern | When it fires |
|---|---|
| Debounce | After events stop for N ms |
| Throttle | At 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 case | Why debounce |
|---|---|
| Search input | Wait for typing to stop |
| Auto-save | Save when editing pauses |
| Form validation | Validate after user pauses |
| Resize recalculation | Recalculate once after drag |
| API calls on input | Prevent request storms |
| localStorage writes | Avoid 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:
| Aspect | Debounce | Throttle |
|---|---|---|
| Fires when | After quiet | During burst, on schedule |
| Frequency | Once | At most N per interval |
| Delays first call? | Yes (trailing) | No |
| Misses last event? | No | Sometimes (basic version) |
| Live feedback | ❌ | ✅ |
| Final state matters | ✅ | Only 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
thiscontext — 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
| Feature | Behavior |
|---|---|
| Fires | After quiet |
| Delay | Reset on each call |
| Live feedback | ❌ |
| Trailing default | ✅ |
| Leading variant | Optional |
| Cancel method | Should provide |
Throttle
| Feature | Behavior |
|---|---|
| Fires | At most N per interval |
| First call | Immediate |
| Live feedback | ✅ |
| Trailing variant | Optional |
| rAF variant | For visuals |
When to Use
| Scenario | Pattern |
|---|---|
| Search input | Debounce |
| Auto-save | Debounce |
| Form validation | Debounce |
| Resize layout | Debounce |
| Scroll progress | Throttle |
| Mousemove tooltip | Throttle |
| Touch drag | Throttle |
| Animation | rAF |
| Button clicks | Leading debounce |
| API polling | Throttle |
| Combined quiet + max | Debounce maxWait |
Common Delays
| Use | Delay |
|---|---|
| Search input | 300ms |
| Auto-save | 500ms |
| Resize | 200ms |
| Scroll progress | 100ms |
| Mouse tooltip | 16ms |
| Touch drag | 50ms |
| Button | 1000ms |
Debounce vs Throttle
| Aspect | Debounce | Throttle |
|---|---|---|
| Fires during burst | ❌ | ✅ |
| Fires after burst | ✅ | Trailing only |
| Cancels pending | ✅ (resets) | ❌ |
| Guaranteed rate | ❌ | ✅ |
| Guaranteed final | ✅ | Trailing only |
| Live updates | ❌ | ✅ |
Variants
| Variant | Behavior |
|---|---|
| Trailing debounce | Runs after quiet |
| Leading debounce | Runs immediately |
| Both | Runs immediately + at end |
| Max-wait debounce | Guaranteed max delay |
| Basic throttle | Leading edge only |
| Trailing throttle | Guaranteed last call |
| rAF throttle | Synced 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
| Pitfall | Problem | Solution |
|---|---|---|
| Debounce on scroll | Freezes until stop | Throttle |
| Throttle on input | Fires mid-typing | Debounce |
| Missing cancel | Timer leaks | Add .cancel() |
Lost this | Method breaks | Arrow / bind |
| Lost arguments | Wrong args passed | Forward ...args |
| rAF not cancelled | Runs after unmount | Cancel handle |
| Delay too short | Work still too frequent | Increase delay |
| Delay too long | Feels laggy | Decrease delay |
| No trailing throttle | Last event dropped | Use 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
| Concept | Syntax | Purpose |
|---|---|---|
| Debounce | debounce(fn, delay) | Run after quiet |
| Throttle | throttle(fn, limit) | Run at fixed rate |
| Leading | Immediate first call | Debounce variant |
| Trailing | Run after last event | Default |
| Cancel | .cancel() | Stop pending |
| rAF throttle | requestAnimationFrame | Sync to frames |
| Max wait | debounce(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
requestAnimationFrameis 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
thisin 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!