Clean Markup

Implementing Debounce and Throttle in Vanilla JavaScript

Implementing Debounce and Throttle in Vanilla JavaScript

Attaching heavy DOM operations or network requests directly to scroll, resize, or keyup events can fire dozens of times per second and freeze the browser's main thread. Implementing debounce and throttle in vanilla JavaScript lets you control how often a handler executes without pulling in an external utility library.

Example

User types "clean" in 300ms (5 keyup events):
- Without debounce: 5 API calls fired
- With debounce(fn, 250): 1 API call fired 250ms after typing stops

Input : Target callback function fn and delay in milliseconds wait
Output : Rate-limited wrapper function preserving this context and arguments.

Implementation

// Debounce: Waits until `wait` ms have elapsed since the last invocation
function debounce(fn, wait) {
  var timeoutId = null;

  return function () {
    var context = this;
    var args = arguments;

    clearTimeout(timeoutId);
    timeoutId = setTimeout(function () {
      fn.apply(context, args);
    }, wait);
  };
}

// Throttle: Executes at most once every `limit` ms
function throttle(fn, limit) {
  var inThrottle = false;

  return function () {
    var context = this;
    var args = arguments;

    if (!inThrottle) {
      fn.apply(context, args);
      inThrottle = true;
      setTimeout(function () {
        inThrottle = false;
      }, limit);
    }
  };
}

// Usage Example on DOM Events
var searchInput = document.querySelector("#search-input");
if (searchInput) {
  searchInput.addEventListener(
    "input",
    debounce(function (event) {
      console.log("Fetching search results for:", event.target.value);
    }, 300)
  );
}

Main Point: Closures allow timeoutId and inThrottle state to persist privately across event invocations while keeping the global namespace clean.

Credits

Photo by Clement H

← Home