Learn how to limit function calls in high-frequency events like scroll and resize, no Lodash, no magic.

Introduction
You’ve probably seen this before:
window.addEventListener("scroll", () => {
console.log("scrolling...");
});
Now open the console and scroll a little.
🔥 Boom, hundreds of logs per second.
That’s because the scroll
event fires continuously.
If you run heavy code (like recalculating layout, fetching data, or updating animations), it’ll quickly kill performance.
The solution? A throttle function is a simple utility that makes sure your function only runs once every X milliseconds, no matter how many times it’s triggered.
Let’s build one from scratch in just 3 steps.
Step 1: Understand the Core Idea
Imagine you’re a barista making coffee.
You can’t handle 100 orders at once. You serve one, wait a bit, then take the next.
That’s throttling.
Technically speaking:
Throttling ensures a function executes at most once per time window, even if it’s called repeatedly.
Visually:
Event: |--------|------|------|--------|------|-------|-------|
Throttle: Run ✅ Skip Skip Run ✅ Skip Skip Run ✅
Simple rule: ignore extra calls until time has passed.
Step 2: Implement the Basic Version`
Let’s code it.
We’ll use timestamps to track when the function was last executed.
function throttle(fn, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
fn.apply(this, args);
}
};
}
if (now - lastCall >= delay) {
lastCall = now;
fn.apply(this, args);
}
};
}
✅ Works with any function
✅ Executes immediately on first call
✅ Ignores subsequent calls until delay
time passes
Usage:
const handleScroll = throttle(() => {
console.log("Scroll event:", window.scrollY);
}, 300);
window.addEventListener("scroll", handleScroll);
Now, even if the scroll fires 200 times per second,
your handleScroll()
runs only once every 300ms.
Step 3: Make It Smarter (Trailing Execution)
The simple version skips extra calls entirely, which might feel unresponsive at the end of an event burst.
Let’s improve it to run once more after the final call, a “trailing edge” execution.
function throttle(fn, delay) {
let lastCall = 0;
let timeout;
return function (...args) {
const now = Date.now();
const remaining = delay - (now - lastCall);
if (remaining <= 0) {
clearTimeout(timeout);
lastCall = now;
fn.apply(this, args);
} else if (!timeout) {
timeout = setTimeout(() => {
lastCall = Date.now();
timeout = null;
fn.apply(this, args);
}, remaining);
}
};
}
✅ Runs immediately, then again after the last burst of activity
✅ Balances responsiveness with performance
✅ Equivalent to Lodash’s default throttle behavior
Usage:
const onResize = throttle(() => {
console.log("Resized:", window.innerWidth);
}, 500);
window.addEventListener("resize", onResize);
Now your resize handler runs both smoothly and efficiently, even if users keep dragging the window.
Bonus: Understanding What’s Happening
Let’s walk through what the code actually does:
lastCall
Stores when the function was last executed.- Every time the function runs, we check how much time has passed.
- If enough time (
delay
) has passed → run immediately. - If not → schedule a trailing call using.
setTimeout()
. - Clear any old timeouts to prevent stacking.
This ensures the function executes at most once per delay,
but also captures the last call in a burst of rapid events.
Real-World Scenarios
✅ Scroll Tracking: Update scroll progress or trigger lazy loading.
✅ Resize Events: Recalculate layout efficiently.
✅ Mouse Move: Animate elements without frame drops.
✅ Drag and Drop: Update positions smoothly but sparingly.
✅ API Rate Limiting: Prevent hitting the backend too frequently.
Example:
const updateProgress = throttle(() => {
const scrollTop = window.scrollY;
const max = document.body.scrollHeight - window.innerHeight;
const percent = Math.round((scrollTop / max) * 100);
console.log(percent + "% scrolled");
}, 200);
window.addEventListener("scroll", updateProgress);
Throttle vs Debounce Quick Recap

👉 Throttle = “limit rate”
👉 Debounce = “wait until quiet”
Conclusion
Building a throttle function from scratch teaches you more than just a trick; it helps you think like the JS engine.
✅ 3 Steps Recap:
- Understand the timing logic (ignore frequent calls).
- Implement timestamp-based throttling.
- Add trailing execution for smoother UX.
Once you know this pattern, you’ll spot throttling opportunities everywhere from event listeners to API calls.
Throttle smart, not hard. Your users (and CPU) will thank you.
Call to Action
Have you ever written your own throttle function or faced performance issues due to frequent events?
Drop your experience or your version throttle()
in the comments 👇
And if your teammate’s scroll handler fires 300 logs per second, share this post, they’ll finally understand how to stop the madness.
Leave a Reply