What is Throttle?
Throttle: Throttling limits how often a function can run, to at most once per interval, so a scroll or pointer handler fires on a steady schedule, not on every event.
To throttle a handler is to cap its rate. Scroll, resize, pointer move and drag events can fire far more often than a screen can update, and a handler that does real work on each one, measuring layout or setting React state, stalls the page. A throttled handler runs at most once per interval, say every 100 milliseconds, with the latest event's data, and drops the rest. Unlike debouncing, which waits for silence, throttling keeps producing updates while the events continue, which is what you want for anything the user watches as it moves.
When to use it
- Following continuous input: a sticky header that reacts to scroll direction, a reading progress bar, a cursor spotlight, a drag preview.
- Rate-limiting outbound work: sending analytics or position updates to a server no more than a few times a second.
- Expensive reads during motion, such as
getBoundingClientRecton scroll. Better still, avoid the read entirely with the observers below.
Prefer the browser's own scheduling
For visual updates, the right interval is one frame, and requestAnimationFrame is the throttle. Store the latest event values in a ref and let a single frame callback write to the DOM; dozens of pointer events per frame collapse into one update. Many classic scroll handlers can go away entirely: IntersectionObserver reports when an element enters the viewport, ResizeObserver when it changes size, and CSS position: sticky and scroll snap handle pinning and paging with no JavaScript. Mark scroll and touch listeners { passive: true } so the browser does not wait for them before scrolling.
How to build one in React
"use client"
import * as React from "react"
/** Runs fn with the latest arguments at most once per animation frame. */
export function useFrameThrottle<A extends unknown[]>(fn: (...args: A) => void) {
const latest = React.useRef<A | null>(null)
const frame = React.useRef(0)
React.useEffect(() => () => cancelAnimationFrame(frame.current), [])
return React.useCallback(
(...args: A) => {
latest.current = args
if (frame.current) return
frame.current = requestAnimationFrame(() => {
frame.current = 0
if (latest.current) fn(...latest.current)
})
},
[fn]
)
}
// const onMove = useFrameThrottle((x: number, y: number) => el.style.setProperty("--x", x + "px"))
// <div onPointerMove={(e) => onMove(e.clientX, e.clientY)} />
Writing to a CSS variable or a transform inside the frame, instead of calling setState, keeps React out of the hot path; the component re-renders only when something the user reads changes. When React state is required, throttle the state updates to a coarse interval and let the visual follow a CSS transition.
Common mistakes
- Throttling with `setTimeout` for visuals, which runs out of step with the display and looks jittery. Use frames.
- Dropping the last event. A trailing call matters: if the user stops scrolling between ticks, the final position must still be applied.
- Throttling a handler that calls `preventDefault`, such as a drag handler, which then only prevents some of the events.
- Throttling instead of fixing the work. If the handler reads layout every tick, it is still too slow. Cache measurements and use observers.
MiniDev's LightProvider follows this pattern: a passive pointermove listener only records the latest coordinates, and a single frame loop eases the light toward them and writes the variables that every shadow reads. BackToTop uses a passive scroll listener, and ScrollProgress drives its bar from a motion value so scrolling never re-renders React.