Skip to content
⚠️ This article was written in 2018. Some content may be outdated.

Debounce and Throttle: Principles, Implementations, and Use Cases

Debounce and throttle are fundamental frontend optimization tools used in almost every project — and a must-know for interviews. This article explains the differences and when to use each.

The Problem

Some events fire at extremely high frequencies:

  • scroll: may fire dozens of times per scroll
  • resize: fires continuously as the window resizes
  • input: fires once per keystroke
  • mousemove: fires every frame as the mouse moves

If the callback executes on every event (especially network requests or DOM operations), performance degrades.

Throttle

Definition: No matter how many times the event fires within a given interval, the callback executes only once.

Analogy: A flow-restricted faucet — at most one drip per minute, no matter how much you open the tap.

javascript
function throttle(fn, delay) {
  let lastTime = 0;

  return function (...args) {
    const now = Date.now();

    if (now - lastTime >= delay) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

// Usage: scroll event fires at most once every 200ms
window.addEventListener(
  "scroll",
  throttle(() => {
    console.log("scroll position:", window.scrollY);
  }, 200),
);

The timestamp version executes at the start of each interval (without waiting for the last trigger).

Timer version (executes at the end of the interval):

javascript
function throttle(fn, delay) {
  let timer = null;

  return function (...args) {
    if (timer) return; // still waiting, ignore

    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null;
    }, delay);
  };
}

Use cases:

  • Scroll-to-load (check every 300ms if the bottom has been reached)
  • Prevent duplicate button clicks (trigger only once in 3 seconds)
  • Mouse-follow animations
  • API polling frequency control

Debounce

Definition: After the event stops firing, wait a specified time before executing the callback. If the event fires again during the wait, the timer resets.

Analogy: Elevator doors — if someone enters, the timer resets; the doors close only after no one has entered for a while.

javascript
function debounce(fn, delay) {
  let timer = null;

  return function (...args) {
    // Clear the previous timer
    if (timer) clearTimeout(timer);

    // Reset the timer
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null;
    }, delay);
  };
}

// Usage: search only after the user stops typing for 500ms
const searchInput = document.getElementById("search");
searchInput.addEventListener(
  "input",
  debounce((e) => {
    fetchSearchResults(e.target.value);
  }, 500),
);

Immediate execution version (executes on the first trigger, then cools down):

javascript
function debounce(fn, delay, immediate = false) {
  let timer = null;

  return function (...args) {
    const callNow = immediate && !timer;

    if (timer) clearTimeout(timer);

    timer = setTimeout(() => {
      timer = null;
      if (!immediate) fn.apply(this, args);
    }, delay);

    if (callNow) fn.apply(this, args);
  };
}

Use cases:

  • Real-time search (request only after typing stops)
  • Form validation (validate only after typing stops)
  • Recalculate layout after window resize ends
  • Auto-save after editor content changes

Comparison

ThrottleDebounce
When it executesAt fixed intervalsAfter the event stops
Best forContinuous response neededWaiting for an action to finish
ExampleScroll position updatesSearch box autocomplete

Core difference: Throttle cares about execution frequency; debounce cares about whether an action is complete.

Using in Vue

vue
<script>
import { debounce, throttle } from "lodash";

export default {
  data() {
    return { searchQuery: "" };
  },
  created() {
    // Create in 'created' so each component instance has its own debounce function
    this.debouncedSearch = debounce(this.fetchResults, 500);
  },
  beforeDestroy() {
    // Cancel any pending calls when the component is destroyed
    this.debouncedSearch.cancel();
  },
  methods: {
    onInput(value) {
      this.searchQuery = value;
      this.debouncedSearch(value);
    },
    async fetchResults(query) {
      const results = await searchAPI(query);
      this.results = results;
    },
  },
};
</script>

Note: Don't wrap with debounce() directly inside methods — that causes all component instances to share the same debounce function:

javascript
// ❌ Wrong: functions in methods are shared across instances
methods: {
  onInput: debounce(function(value) { ... }, 500)
}

// ✅ Correct: create in 'created', each instance is independent
created() {
  this.debouncedFn = debounce(this.fn, 500)
}

Summary

  • High-frequency events must be throttled or debounced, or you'll have performance issues
  • Throttle = execute at a fixed rate (for scenarios requiring continuous response)
  • Debounce = execute after stopping (for scenarios waiting for an action to complete)
  • In Vue: create in created, cancel in beforeDestroy

MIT Licensed