Skip to main content

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 leading and trailing options, a call at the start and at the end of the interval.

Quick example

javascript
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:

javascript
window.addEventListener('mousemove', () => { console.log('mouse move'); });

What goes wrong:

  • mousemove can 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

javascript
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:

  1. On the first call the function runs immediately.
  2. The timestamp of the last call is stored (lastCall).
  3. All subsequent calls are ignored until delay milliseconds have passed.
  4. 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

  1. Updating a progress bar on scroll:

    javascript
    window.addEventListener( 'scroll', throttle(() => { progressBar.value = window.scrollY; }, 100) );
  2. Tracking the mouse position for an animation:

    javascript
    window.addEventListener( 'mousemove', throttle(updateCursorPosition, 50) );
  3. Recalculating window sizes (resize):

    javascript
    window.addEventListener( 'resize', throttle(updateLayout, 200) );

Many projects take the ready-made variant from Lodash:

javascript
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

FeatureThrottleDebounce
GoalLimit the call frequencyPostpone execution until the user has finished
FiresRegularly, but no more often than once per N msOnly once after a pause
Examplescroll, mousemove, resizeinput, 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:

ItemDescription
What it doesLimits how often a function is called
When to use itFor frequent events: scroll, resize, mousemove
ArgumentsA function and a delay (ms)
ReturnsA new wrapped function
BenefitReduces CPU load and improves performance
Difference from debounceRuns 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 with lastCall at 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 setTimeout at 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, so performance.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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.