Throttling guarantees a function runs at most once per fixed time interval, no matter how often it's triggered — the standard tool for scroll handlers, mouse-move tracking, and any continuously-firing event that needs a steady, capped execution rate.
1Throttle | JavaScript Tutorial - In-Depth Guide Part 1
Throttling ensures a function executes at most once every fixed interval, regardless of how many times it's actually called during that window.
function throttle(fn, interval) {
let lastCall = 0;
return (...args) => {
const now = Date.now();
if (now - lastCall >= interval) {
lastCall = now;
fn(...args);
}
};
}Limiting Call Frequency
2Throttle | JavaScript Tutorial - In-Depth Guide Part 2
The fundamental difference from debounce: throttle guarantees the function runs periodically DURING continuous activity, while debounce only runs it once AFTER activity stops.
// Throttle: fires repeatedly during continuous scrolling
// Debounce: fires once, after scrolling stopsThrottle vs Debounce
3Throttle | JavaScript Tutorial - In-Depth Guide Part 3
A basic throttle implementation (timestamp comparison) only fires on the leading edge of each interval — a final call right at the end of a burst might be dropped if it doesn't land exactly on an interval boundary.
function throttleWithTrailing(fn, interval) {
let lastCall = 0, timeoutId;
return (...args) => {
const now = Date.now();
const remaining = interval - (now - lastCall);
if (remaining <= 0) {
lastCall = now;
fn(...args);
} else {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => { lastCall = Date.now(); fn(...args); }, remaining);
}
};
}Ensuring the Last Call Fires
4Throttle | JavaScript Tutorial - In-Depth Guide Part 4
Throttling a scroll or mousemove handler prevents expensive layout calculations from running on every single fired event, which can number in the hundreds per second.
window.addEventListener('scroll', throttle(() => {
updateStickyHeaderPosition();
}, 100));Throttling Scroll Handlers
5Throttle | JavaScript Tutorial - In-Depth Guide Part 5
For animations tied to scroll or resize, requestAnimationFrame is often an even better fit than a fixed-interval throttle, since it naturally syncs to the browser's actual repaint rate.
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
updateParallaxEffect();
ticking = false;
});
ticking = true;
}
});requestAnimationFrame Throttling
6Step-by-Step Breakdown
Throttling ensures a function executes at most once every fixed interval, regardless of how many times it's actually called during that window.
The fundamental difference from debounce: throttle guarantees the function runs periodically DURING continuous activity, while debounce only runs it once AFTER activity stops.
Checkpoint: During 5 seconds of continuous, rapid activity, does a throttled function fire multiple times, or just once at the end?
- →Multiple times, spaced roughly by the interval
- →Just once, after the activity stops
A basic throttle implementation (timestamp comparison) only fires on the leading edge of each interval — a final call right at the end of a burst might be dropped if it doesn't land exactly on an interval boundary.
Throttling a scroll or mousemove handler prevents expensive layout calculations from running on every single fired event, which can number in the hundreds per second.
Checkpoint: Is throttling a scroll handler a common technique to prevent performance jank?
- →Yes, scroll events can fire far more often than needed
- →No, scroll events already fire at a safe, limited rate natively
For animations tied to scroll or resize, requestAnimationFrame is often an even better fit than a fixed-interval throttle, since it naturally syncs to the browser's actual repaint rate.
Next, we'll explore 'requestAnimationFrame'.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Throttle Scroll-Triggered Focus or Announcement Changes to Avoid Overwhelming Screen Readers
If scroll position drives dynamic ARIA live region updates (like a 'reading progress' announcement), throttle those updates significantly (much slower than a visual scroll indicator) to avoid firing rapid, overlapping announcements that overwhelm screen reader users.
SEO Implications
- 1
Throttled Scroll Handlers Improve Scroll Responsiveness Metrics
Reducing the computational load of scroll event handlers via throttling helps maintain smooth scrolling and responsiveness, contributing positively to Interaction to Next Paint and overall perceived performance.
Best Practices
Throttle Continuously-Firing Events Like Scroll and Mousemove
These events can fire far more often than any handler realistically needs to respond to, and throttling caps the execution rate to something the main thread can comfortably keep up with.
Consider requestAnimationFrame Instead of a Fixed Interval for Visual Updates
It naturally throttles to the display's actual refresh rate and avoids wasted work computing visual updates faster than the screen can render them.
Frequent Bugs
Using debounce instead of throttle for a scroll-position indicator, causing it to update only once scrolling stops, instead of continuously as the user scrolls.
Use throttle when you need periodic updates during continuous activity; reserve debounce for cases where only the final state after activity stops matters.
Using a basic leading-edge-only throttle for a progress indicator, causing the very last update (e.g. reaching 100%) to sometimes never render if it falls within an unfired interval window.
Use a throttle implementation that also guarantees a trailing-edge call, ensuring the final state is never dropped.
Real-World Examples
Throttling a Sticky Header Show/Hide Effect
A page needed to hide a sticky header when scrolling down and show it when scrolling up, without recalculating scroll direction on every single fired scroll event.
let lastScrollY = 0;
window.addEventListener('scroll', throttle(() => {
const direction = window.scrollY > lastScrollY ? 'down' : 'up';
header.classList.toggle('hidden', direction === 'down');
lastScrollY = window.scrollY;
}, 150));