Advanced JS

Debounce and Throttle

Learn delay decorators, this-preserving timers, setTimeout, setInterval, debounce, and throttle in JavaScript.

JavaScript Debounce, Throttle, and Timer Delays

This page covers timer-based control flow in JavaScript: delaying function calls, preserving or losing this after a delay, and limiting repeated calls with debounce and throttle decorators.

Delay Decorator

This decorator wraps a function and delays its execution by a specified number of milliseconds.

It preserves this with Function.prototype.apply, so the same delayed method can be attached to different objects without losing context.

const obj1 = {
  info: "obj1 info",
  showInfo(...args) {
    console.log(`${args}: ${this.info}`);
  },
};

const obj2 = {
  info: "obj2 info",
};

function delay(f, ms) {
  return function (...args) {
    setTimeout(() => {
      // Uses apply to handle `this` dynamically
      f.apply(this, args);
    }, ms);
  };
}

obj1.delayedShowInfo = delay(obj1.showInfo, 1000);
obj2.delayedShowInfo = delay(obj1.showInfo, 1000);

obj1.delayedShowInfo("Info", "one"); // Info,one: obj1 info
obj2.delayedShowInfo("Info"); // Info: obj2 info

Delaying function execution with and without context (this)

This version delays a standalone function with setTimeout, but it does not preserve this when used with object methods.

For object methods, Function.prototype.bind or apply should be used to retain proper context.

function delay(f, ms) {
  return function (...args) {
    setTimeout(() => {
      f(...args);
    }, ms);
  };
}

function showDetails(name, age) {
  console.log(`Name: ${name}, Age: ${age}`);
}

const delayedShowDetails = delay(showDetails, 1000);
delayedShowDetails("Alice", 25);
// Name: Alice, Age: 25

const obj1 = {
  info: "obj1 info",
  showInfo(prefix) {
    console.log(`${prefix}: ${this?.info}`);
  },
};

obj1.delayedShowInfo = delay(obj1.showInfo, 1000);
obj1.delayedShowInfo("Info", "one"); // Info: undefined

Output Every Second

Both versions print numbers at one-second intervals. setInterval repeats until cleared. Recursive setTimeout schedules the next run only after the current run finishes, which gives better control when work duration can vary.

setInterval

function printNumbers(from, to) {
  let current = from;
  let timerId;

  function go() {
    console.log(current);
    if (current === to) {
      clearInterval(timerId);
    }
    current++;
  }

  go();
  timerId = setInterval(go, 1000);
}

printNumbers(5, 10);
// 5 immediately, and 6 to 10 with 1s between

setTimeout

function printNumbers(from, to) {
  let current = from;

  function go() {
    console.log(current);
    if (current < to) {
      setTimeout(go, 1000);
    }
    current++;
  }

  go();
}

printNumbers(5, 10);
// 5 immediately, and 6 to 10 with 1s between

Throttle and Debounce Decorators

Debounce and throttle both limit calls, but they answer different questions:

  • debounce waits for quiet time, then runs once.
  • throttle allows a call immediately, then blocks further calls until the time window ends.

debounce

function debounce(func, ms) {
  let timeout;

  return function (...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), ms);
  };
}

const timeLoggedConsoleLog = (...args) => {
  console.log(`Logged after ${Date.now() - startTime} ms:`, ...args);
};

const startTime = Date.now();
const f = debounce(timeLoggedConsoleLog, 500);

f("a");
setTimeout(() => f("b"), 200);
setTimeout(() => f("c"), 600);
setTimeout(() => f("d"), 600);
setTimeout(() => f("e"), 600); // Logged after 1118 ms: e

throttling

function throttle(fn, limit) {
  let inThrottle;

  return function (...args) {
    if (inThrottle) return;
    fn.apply(this, args);
    inThrottle = true;
    setTimeout(() => (inThrottle = false), limit);
  };
}

const timeLoggedConsoleLog = (...args) => {
  console.log(`Logged after ${Date.now() - startTime} ms:`, ...args);
};

const startTime = Date.now();
const f = throttle(timeLoggedConsoleLog, 500);

f("a"); // Logged after 0 ms: a
setTimeout(() => f("b"), 200);
setTimeout(() => f("c"), 600); // Logged after 613 ms: c
setTimeout(() => f("d"), 600);
setTimeout(() => f("e"), 600);

On this page