Throttle in JavaScript
throttle is a technique that limits how often a function is called: even when an event happens very frequently, the function runs no more often than once per a given time interval. In plain words the rule is: "call the function no more often than once every N milliseconds".
Theory
TL;DR
- The throttle rule: "no more often than once per N ms", whatever the number of events.
- The implementation keeps the timestamp of the last call (
lastCall) and lets a new one through only once the interval has passed. - The first call goes through immediately, the following ones are ignored until the delay window closes.
- Scenarios:
scroll,mousemove,resize, infinite scroll, progress bar updates. - Throttle means "no more often than"; debounce means "no earlier than". The first updates during the action, the second after it.
- The Lodash version adds the
leadingandtrailingoptions, a call at the start and at the end of the interval.
Quick example
function throttle(fn, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
fn.apply(this, args);
}
};
}
const handleMove = throttle(() => {
console.log('Mouse move handled');
}, 500);
window.addEventListener('mousemove', handleMove);The problem: a handler without throttle
Suppose you want to track how the user moves the mouse:
window.addEventListener('mousemove', () => {
console.log('mouse move');
});What goes wrong:
mousemovecan fire dozens of times per second (up to 100-200 times per second);- the function is called far too often;
- CPU load grows and performance drops.
If the handler reads geometry (getBoundingClientRect) or changes styles, every extra firing also triggers another layout pass, so the page starts to jank.
How throttle works internally
function throttle(fn, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
fn.apply(this, args);
}
};
}
const handleMove = throttle(() => {
console.log('Mouse move handled');
}, 500);
window.addEventListener('mousemove', handleMove);Now handleMove is called no more often than once per 500 ms, even if mousemove fires hundreds of times.
Step by step:
- On the first call the function runs immediately.
- The timestamp of the last call is stored (
lastCall). - All subsequent calls are ignored until
delaymilliseconds have passed. - Once the time window closes, the function can be called again.
Note that this simplest implementation has leading behaviour only. The last event of a burst that lands inside the interval is lost, because there is no trailing call here.
Examples from real code
-
Updating a progress bar on scroll:
javascriptwindow.addEventListener( 'scroll', throttle(() => { progressBar.value = window.scrollY; }, 100) ); -
Tracking the mouse position for an animation:
javascriptwindow.addEventListener( 'mousemove', throttle(updateCursorPosition, 50) ); -
Recalculating window sizes (
resize):javascriptwindow.addEventListener( 'resize', throttle(updateLayout, 200) );
Many projects take the ready-made variant from Lodash:
import { throttle } from 'lodash';
const throttled = throttle(() => {
console.log('scroll!');
}, 300);
window.addEventListener('scroll', throttled);The Lodash version is more advanced: it can call the function at the start and/or at the end of the interval through the leading and trailing options, and it has a cancel() method to drop a pending call.
The difference from debounce
| Feature | Throttle | Debounce |
|---|---|---|
| Goal | Limit the call frequency | Postpone execution until the user has finished |
| Fires | Regularly, but no more often than once per N ms | Only once after a pause |
| Example | scroll, mousemove, resize | input, search, autocomplete |
| Behaviour | "No more often than" | "No earlier than" |
Concretely:
- Throttle: while the page scrolls, update the position every 100 ms.
- Debounce: wait until the user stops typing, and only then send the request.
Summary:
| Item | Description |
|---|---|
| What it does | Limits how often a function is called |
| When to use it | For frequent events: scroll, resize, mousemove |
| Arguments | A function and a delay (ms) |
| Returns | A new wrapped function |
| Benefit | Reduces CPU load and improves performance |
| Difference from debounce | Runs regularly, but no more often than the given interval |
Common mistakes
- Creating the wrapper on every firing:
addEventListener('scroll', throttle(fn, 100))inside another handler or a render produces a new function withlastCallat zero each time, so the limit never applies. - Forgetting the lost trailing call: in the naive implementation the last event of a burst is not handled, so the final scroll position may never be recorded. Use Lodash for that, or add a
setTimeoutat the tail of the interval. - Confusing throttle with debounce: for a live search, throttle sends a request every N ms while the user is still typing, when what you want is one request after the pause.
- Using throttle for animation instead of
requestAnimationFrame: for visual updates, rAF is synchronised with browser frames and gives a smoother result. - Using
Date.now()where monotonic time is required: the system clock can be adjusted, soperformance.now()is safer for precise intervals. - Not removing the listener when a component unmounts: throttle does not cancel the subscription, and the wrapper keeps driving code for a screen that no longer exists.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.