You build a small React dashboard and notice the browser lags when you resize the window or scroll fast. The charts stutter, buttons freeze, and users get annoyed. I hit this exact problem once β charts were re-rendering on every tiny resize event and users with slower machines felt like they were dragging through molasses.
The fix was small but effective: throttling. We limited how often the expensive work ran, and the app felt smooth again. In this post, Iβll explain why throttling matters, how it works, and how to use it in React β in plain English.
π§© Why throttling matters (the βwhyβ before the βhowβ)
Think of throttling like a traffic cop at a busy crosswalk. Without the cop, everyone rushes across and causes chaos. With the cop, one person crosses every few seconds β orderly and predictable.
In web apps, events like scroll
, resize
, mousemove
, and input
can fire dozens or hundreds of times per second. If your handler does heavy work (re-render, compute layout, call API), running it on every event will:
- slow down the browser (CPU spikes),
- cause many unnecessary network calls, and
- make the UI feel janky.
Throttling gives you control: run the handler at most once every N milliseconds. That reduces work and keeps the UI responsive.
π§ Throttle vs Debounce β quick friendly analogy
-
Throttle: A guard lets one person through every N seconds. Even if 100 people wait, one goes through on schedule.
Good for:
scroll
,resize
, periodic analytics. - Debounce: A timer waits for silence, then lets the last person through. If people keep coming, it keeps waiting. Good for: search-as-you-type where you want to wait until typing stops.
Both are useful β pick based on the problem.
π οΈ Simple throttle implementation (plain JS)
Letβs build a small, easy-to-read throttle function.
// throttle.js
function throttle(fn, wait = 200) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= wait) {
lastCall = now;
fn.apply(this, args);
}
};
}
export default throttle;
π Why this works (step-by-step)
-
lastCall
stores whenfn
last ran. - On each event, we check current time (
now
). - If
now - lastCall >= wait
, we runfn
and updatelastCall
. - Otherwise, we ignore this event.
This is a leading-edge throttle β it runs immediately, then blocks until wait
ms pass.
π React use case: Throttle window resize
Imagine you want to update a chart layout on window resize. Resizing fires continuously as a user drags the corner β we only need to update at most once every 200ms.
// useWindowSizeThrottled.jsx
import { useEffect, useState } from "react";
import throttle from "./throttle";
export default function useWindowSizeThrottled(wait = 200) {
const isClient = typeof window !== "undefined";
const [size, setSize] = useState({
width: isClient ? window.innerWidth : 0,
height: isClient ? window.innerHeight : 0,
});
useEffect(() => {
const handleResize = throttle(() => {
setSize({ width: window.innerWidth, height: window.innerHeight });
}, wait);
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [wait]);
return size;
}
β Step-by-step breakdown
-
useState
keeps the current window size. - Inside
useEffect
, we create a throttledhandleResize
using ourthrottle
helper. - We add
handleResize
to theresize
event. - On cleanup, we remove the listener to avoid leaks.
This prevents setSize
from being called dozens of times per second, reducing re-renders and CPU usage.
βοΈ Improve the throttle for React (useRef approach)
If your component re-renders often, recreating the throttle could cause surprising behavior. useRef
keeps stable state without causing re-renders:
import { useEffect, useRef, useState } from "react";
function useWindowSizeThrottledRef(wait = 200) {
const isClient = typeof window !== "undefined";
const [size, setSize] = useState({
width: isClient ? window.innerWidth : 0,
height: isClient ? window.innerHeight : 0,
});
const lastCallRef = useRef(0);
useEffect(() => {
function handler() {
const now = Date.now();
if (now - lastCallRef.current >= wait) {
lastCallRef.current = now;
setSize({ width: window.innerWidth, height: window.innerHeight });
}
}
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, [wait]);
return size;
}
β¨ Why useRef
helps
-
lastCallRef
persists across renders without triggering re-renders. - The handlerβs internal state remains stable across the component lifecycle.
β οΈ Common mistakes and misconceptions
- Confusing throttle with debounce β pick based on need.
- Not cleaning up listeners β always remove event listeners in cleanup.
-
Recreating throttle on every render β leads to losing internal state; use
useRef
oruseCallback
. - Throttling API calls blindly β if users expect instant feedback (search), debouncing may be better.
-
Assuming throttle makes things βreal-timeβ β throttle limits frequency but can delay responsiveness if
wait
is large.
π§Ύ Real-world use cases
- Throttling
scroll
handlers for sticky headers or analytics. - Throttling
resize
events for layout recalculation. - Limiting telemetry or analytics to avoid server-side throttling.
- Rate-limiting cursor-movement handlers in web games.
- Controlling UI updates from frequent streams or websockets.
π― Bonus: How to Explain Throttling in an Interview
When an interviewer asks, βWhat is throttling?β keep it short and show a quick example.
π£οΈ Short answer (30s)
Throttling limits how often a function can run β at most once per given interval β which prevents expensive handlers from running too frequently and improves UI performance.
π» Quick example to show
Sketch or paste the simple throttle(fn, wait)
code and explain lastCall
+ Date.now()
logic.
π Common follow-ups & answers
- Q: How is throttle different from debounce? A: Throttle enforces a steady pace; debounce waits for silence.
- Q: Leading vs trailing throttle? A: Leading runs immediately then waits; trailing runs at the end of the burst. You can implement options for both.
- Q: Edge cases? A: Time drift, handler identity across renders, and missing final call after a burst β be prepared to discuss solutions.
π§© Mini whiteboard challenge
Design throttle(fn, wait, options)
where options
may include leading
and trailing
. Sketch a timeline showing when fn
runs during a burst of events.
π§ Key takeaways
- Throttle = run at most once every N ms β great for periodic limits.
- Debounce = run after events stop β great for waiting until user is done typing.
- In React, keep handlers stable (
useRef
,useCallback
) and always remove listeners. - Choose
wait
by testing on real devices β 100β300ms is a common range for UI work. - Keep user expectations in mind β throttling reduces CPU/network but may delay responsiveness.
π About Me
Hi, I'm Saurav Kumar β a Software Engineer passionate about building modern web and mobile apps using JavaScript, TypeScript, React, Next.js, and React Native.
Iβm exploring how AI tools can speed up development,
and I share beginner-friendly tutorials to help others grow faster.
π Connect with me:
LinkedIn β I share short developer insights and learning tips
GitHub β Explore my open-source projects and experiments
If you found this helpful, share it with a friend learning JavaScript β it might help them too.
Until next time, keep coding and keep learning π
Top comments (0)