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.
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:
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:
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
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:
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.
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:
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:
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:
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:
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:
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:
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:
| Time | Raw calls | Throttle (100ms) | Debounce (100ms) |
|---|---|---|---|
| t=0ms | call 1 | executes | timer reset |
| t=50ms | call 2 | ignored | timer reset |
| t=100ms | call 3 | executes | timer reset |
| t=150ms | call 4 | ignored | timer reset |
| t=200ms | call 5 | executes | timer reset |
| t=300ms | (quiet) | nothing | executes 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
| Pattern | Use when | Example |
|---|---|---|
| Throttle | Need regular updates during activity | Scroll position, game loop, progress bar |
| Debounce | Only care about final state | Search query, auto-save, form validation |
| Rate limit | Need total call cap over a window | API 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
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.
Frequently Asked Questions
What is the difference between throttle and debounce?
What throttle interval should I use?
Does throttle drop calls or queue them?
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.
More in this topic
Using Reflect and Proxy Together in JavaScript
Proxy traps intercept operations, but Reflect is what makes them behave correctly. Learn why Reflect belongs inside every trap and what breaks when you skip it.
Top JS Array Methods Interview Questions to Know
The array method questions that come up most often in JavaScript interviews, answered directly with short examples: map vs forEach, mutating vs non-mutating methods, reduce, and more.
JavaScript Reflect API: Advanced Architecture
The Reflect object exposes JavaScript's own internal operations as plain functions. Learn every Reflect method, what it returns, and why it exists as its own API.