Back to stories
<Frontend/>

Debounce and Throttle in Vue and JavaScript: When to Use Each

Share by

Debounce and Throttle in Vue and JavaScript: When to Use Each

When you react to frequent events (typing, resize, scroll), running your handler on every event can be expensive. Debounce and throttle limit how often the handler runs. This post explains the difference and when to use each, with a simple Vue composable.


Debounce: run after a pause

Debounce waits until the event has stopped for a set delay, then runs the handler once. If another event happens before the delay ends, the timer resets.

Use it for:

  • Search input: Run the search request only after the user stops typing (e.g. 300 ms).
  • Form validation: Validate after the user pauses typing, not on every keystroke.
  • Auto-save: Save after the user stops editing.

Example (conceptual):

function debounce<T extends (...args: unknown[]) => void>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timeoutId: ReturnType<typeof setTimeout>
  return (...args: Parameters<T>) => {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => fn(...args), delay)
  }
}

const onSearch = debounce((query: string) => fetchResults(query), 300)

Throttle: run at most every N ms

Throttle runs the handler at most once every N milliseconds. If events keep coming, the handler runs on a fixed cadence (e.g. every 100 ms), not after a pause.

Use it for:

  • Scroll or resize: Update UI or state at a capped rate (e.g. every 100 ms) so you don't run logic on every frame.
  • Click / submit: Prevent double-submit by ignoring repeated clicks within a short window.
  • Progress or position: Sample position or progress at a steady rate instead of every event.

Example (conceptual):

function throttle<T extends (...args: unknown[]) => void>(
  fn: T,
  limit: number
): (...args: Parameters<T>) => void {
  let last = 0
  return (...args: Parameters<T>) => {
    const now = Date.now()
    if (now - last >= limit) {
      last = now
      fn(...args)
    }
  }
}

const onScroll = throttle(() => updateScrollPosition(), 100)

Comparison

DebounceThrottle
When it runsAfter activity stops for delay msAt most once every limit ms while activity continues
Typical useSearch, validation, auto-saveScroll, resize, rate-limited clicks

Vue composable example

You can wrap debounce or throttle in a composable and use it in onMounted / onUnmounted so timers are cleared when the component unmounts:

// composables/useDebounce.ts
import { ref, onUnmounted } from 'vue'

export function useDebounce<T>(fn: (value: T) => void, delay: number) {
  let timeoutId: ReturnType<typeof setTimeout>
  onUnmounted(() => clearTimeout(timeoutId))
  return (value: T) => {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => fn(value), delay)
  }
}

Use a similar pattern for throttle, clearing any pending timeout or animation-frame id in onUnmounted.


Summary

  • Debounce: run once after the user (or event stream) stops for a given delay. Best for search, validation, auto-save.
  • Throttle: run at most every N ms while events keep coming. Best for scroll, resize, rate-limited actions.
  • Use a composable in Vue to centralize the logic and clean up timers in onUnmounted so you don't leak or run after unmount.