Throttling in JavaScript: A Complete Tutorial

Learn how to throttle functions in JavaScript to enforce a maximum execution rate. Master scroll handlers, game loops, and real-time UI updates that need consistent firing.

7 min read

Throttling enforces a maximum execution rate on a function. If a function is called rapidly, a throttled version runs at most once per specified interval, no matter how many times it is triggered.

Unlike debouncing, which waits for activity to stop, throttling guarantees regular execution during activity. Think of it as a metronome for your function calls.

The Problem Without Throttling

Scroll events are the classic example. A single scroll gesture fires hundreds of events:

javascriptjavascript
window.addEventListener("scroll", () => {
  console.log("Scroll position:", window.scrollY);
  // Expensive work: update a progress bar, lazy load images, recalculate layout
});

One flick of the scroll wheel produces:

texttext
Scroll position: 0
Scroll position: 12
Scroll position: 36
Scroll position: 72
... (100+ more lines)

Most of those updates are wasted. The user cannot perceive 100 updates in 200 milliseconds. You need to fire regularly, but not on every pixel change.

The Basic Throttle Function

javascriptjavascript
function throttle(fn, interval) {
  let lastTime = 0;
 
  return function (...args) {
    const now = Date.now();
 
    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

Every call checks the time since the last execution. If enough time has passed, the function runs. Otherwise, the call is ignored:

javascriptjavascript
const throttledScroll = throttle(() => {
  console.log("Throttled scroll:", window.scrollY);
}, 200);
 
window.addEventListener("scroll", throttledScroll);

Now the same scroll gesture produces output at most every 200ms, regardless of how many raw scroll events fire.

Throttle: function fires at regular intervals during activity

The first call executes immediately. Subsequent calls are ignored until 200ms have passed since the last execution. The function fires at a steady rate regardless of how fast events arrive.

Throttle with Trailing Edge

The basic throttle fires on the leading edge (first call in each interval). You can add a trailing edge option to also fire at the end of the last interval:

javascriptjavascript
function throttle(fn, interval, options = {}) {
  const { leading = true, trailing = false } = options;
  let lastTime = 0;
  let timerId = null;
 
  return function (...args) {
    const now = Date.now();
 
    if (lastTime === 0 && !leading) {
      lastTime = now;
    }
 
    const remaining = interval - (now - lastTime);
 
    if (remaining <= 0) {
      if (timerId) {
        clearTimeout(timerId);
        timerId = null;
      }
      lastTime = now;
      fn.apply(this, args);
    } else if (trailing && !timerId) {
      timerId = setTimeout(() => {
        lastTime = leading ? Date.now() : 0;
        timerId = null;
        fn.apply(this, args);
      }, remaining);
    }
  };
}

Trailing edge is useful when you want to capture the final state after activity stops:

javascriptjavascript
const throttledScroll = throttle(
  () => console.log("Final scroll:", window.scrollY),
  200,
  { leading: true, trailing: true }
);
 
window.addEventListener("scroll", throttledScroll);

The function fires on the first scroll event and again 200ms after the last scroll event, capturing the final position.

Throttle for Scroll-Based UI

A practical example: updating a table of contents highlight as the user scrolls through a long article:

javascriptjavascript
function createScrollSpy(headings, updateFn) {
  const throttledUpdate = throttle(() => {
    const scrollPos = window.scrollY + 100; // Offset for better UX
 
    let currentId = headings[0]?.id || "";
 
    headings.forEach(heading => {
      if (heading.getBoundingClientRect().top + window.scrollY <= scrollPos) {
        currentId = heading.id;
      }
    });
 
    updateFn(currentId);
  }, 100);
 
  window.addEventListener("scroll", throttledUpdate);
  return () => window.removeEventListener("scroll", throttledUpdate);
}
 
// Usage
const headings = document.querySelectorAll("h2[id], h3[id]");
createScrollSpy(Array.from(headings), (activeId) => {
  document.querySelectorAll(".toc a").forEach(link => {
    link.classList.toggle("active", link.getAttribute("href") === `#${activeId}`);
  });
});

The scroll spy updates every 100ms during scrolling. That is fast enough to feel responsive but avoids the cost of recalculating positions on every single pixel.

requestAnimationFrame for Visual Throttling

For visual updates, requestAnimationFrame is better than setTimeout-based throttle. It syncs with the browser's paint cycle:

javascriptjavascript
function rafThrottle(fn) {
  let ticking = false;
 
  return function (...args) {
    if (!ticking) {
      ticking = true;
      requestAnimationFrame(() => {
        fn.apply(this, args);
        ticking = false;
      });
    }
  };
}

This is the correct throttle for animations, parallax effects, and smooth scroll-linked UI:

javascriptjavascript
const updateParallax = rafThrottle(() => {
  const scrolled = window.scrollY;
  document.querySelector(".parallax-bg").style.transform =
    `translateY(${scrolled * 0.5}px)`;
});
 
window.addEventListener("scroll", updateParallax);

requestAnimationFrame fires at roughly 60fps (every ~16ms). This is the ideal throttle rate for anything visual because it matches the display refresh rate.

Throttle for Game Loops

Games need a consistent update rate. Throttle the game loop to a fixed timestep:

javascriptjavascript
function createGameLoop(update, render, fps = 60) {
  const interval = 1000 / fps;
  let lastTime = 0;
  let animFrameId = null;
 
  function loop(timestamp) {
    animFrameId = requestAnimationFrame(loop);
 
    const delta = timestamp - lastTime;
 
    if (delta >= interval) {
      lastTime = timestamp - (delta % interval);
      update(delta / 1000); // Delta in seconds
      render();
    }
  }
 
  return {
    start() {
      lastTime = performance.now();
      animFrameId = requestAnimationFrame(loop);
    },
    stop() {
      cancelAnimationFrame(animFrameId);
    }
  };
}

The game logic updates at exactly 60fps. The render call draws the current state. If the browser falls behind, the loop skips updates but keeps rendering.

Throttle vs Debounce: Side by Side

Here is the same burst of 5 rapid calls at t=0, 50, 100, 150, 200 with a 100ms delay:

TimeRaw callsThrottle (100ms)Debounce (100ms)
t=0mscall 1executestimer reset
t=50mscall 2ignoredtimer reset
t=100mscall 3executestimer reset
t=150mscall 4ignoredtimer reset
t=200mscall 5executestimer reset
t=300ms(quiet)nothingexecutes call 5

Throttle fires at t=0, t=100, t=200. Three executions during activity. Debounce fires once at t=300, after activity stops.

When to Use Each

PatternUse whenExample
ThrottleNeed regular updates during activityScroll position, game loop, progress bar
DebounceOnly care about final stateSearch query, auto-save, form validation
Rate limitNeed total call cap over a windowAPI request quota, login attempts

All three are forms of rate limiting applied to different timing needs.

Common Mistakes

Using debounce for scroll. Debounced scroll handlers feel broken. The user scrolls, nothing happens, then suddenly the UI jumps. Use throttle for scroll.

Using throttle for search. Throttled search fires while the user is still typing, wasting API calls on partial queries. Use debounce.

Throttle interval too large. A 500ms throttle on a scroll-linked animation looks choppy. For visual updates, use requestAnimationFrame. For data updates, 100-200ms is the sweet spot.

Rune AI

Rune AI

Key Insights

  • Throttling limits a function to run at most once per specified interval.
  • Unlike debounce, throttled functions fire regularly during continuous activity.
  • The standard throttle fires on the leading edge of each interval.
  • requestAnimationFrame is the best throttle for visual updates (60fps).
  • Choose throttle for continuous updates, debounce for final-state updates.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between throttle and debounce?

Throttle guarantees execution at a regular interval during activity. Debounce waits for activity to stop, then executes once. Use throttle when you need continuous updates (scroll position). Use debounce when you only care about the final state (search query).

What throttle interval should I use?

Scroll handlers: 100-200ms. Mouse move: 50-100ms. Game loops: 16ms (60fps) using requestAnimationFrame. API polling: 5000-30000ms. Match the interval to how often the user needs to see updates.

Does throttle drop calls or queue them?

The standard throttle drops intermediate calls. Only the first call in each interval window executes (leading edge). Some implementations also fire a trailing call at the end of the interval.

Conclusion

Throttling enforces a maximum execution rate on a function. Use it when you need consistent updates during rapid activity: scroll tracking, game loops, resize animations, and real-time data streams. It is the sibling of debouncing, and knowing when to use each is the mark of an experienced JavaScript developer.Throttling guarantees a function runs at most once per interval. It is the right tool when the user needs continuous feedback during rapid activity. The basic version fires on the leading edge. Add trailing edge when you need the final state. Use requestAnimationFrame for visual updates. The rule of thumb: if the user needs to see updates during an action, throttle. If the user needs the result after an action stops, debounce.