You know the problem: the search box fires too many requests, or scrolling and resize make the UI janky. Two tools fix this: debounce and throttle. The trick is not memorising code — it’s knowing when to use which.
This article gives you:
- a short decision checklist to pick the right tool,
- a single beginner-friendly mini-project that uses both (so you see the difference),
- plain-JS + React code you can copy, and
- a short interview prep section.
Let’s be practical and keep things small.
✅ Quick decision checklist — when to use which
Use this checklist first. If you can answer “yes” to a line, follow that rule.
- Does the user expect the final value after they stop interacting? → debounce. (search box, autosave, type-to-filter where final query matters)
- Do you need regular updates while the user is interacting, capped at a frequency? → throttle. (scroll position updates, resize/layout recalculation, performance telemetry)
- Do you need the last event after a burst and regular updates during the burst? → consider throttle + trailing or a combined approach.
- Are you protecting server/API costs (many users typing)? → debounce for inputs.
- Is the event happening continuously (scroll/resize) and you want a steady stream of updates? → throttle.
Simple rule:
- final-value ⇒ debounce
- steady-rate ⇒ throttle
Recommended starting delays:
- UI inputs (debounce): 250–500ms
- scroll/resize (throttle): 100–300ms
🔁 Mini project: PhotoSearch (one app, both tools)
Build a small app with:
- a search box that queries (debounced), and
- an infinite scroll that loads more photos while the user scrolls (throttled).
This shows both problems in one place — and teaches decision-making by doing.
Plain JavaScript: core helpers
debounce.js
export function debounce(fn, delay = 300) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
throttle.js
export function throttle(fn, wait = 200) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= wait) {
last = now;
fn.apply(this, args);
}
};
}
PhotoSearch: simple layout (HTML)
<!-- index.html -->
<input id="search" placeholder="Search photos..." />
<div id="gallery" class="gallery"></div>
<script type="module" src="app.js"></script>
styles.css
can be a simple grid — use CSS grid with grid-template-columns: repeat(auto-fill, minmax(180px,1fr));
.
PhotoSearch: app logic (plain JS)
// app.js
import { debounce } from './debounce.js';
import { throttle } from './throttle.js';
let page = 1;
let query = '';
async function fetchPhotos(q = '', pageNo = 1) {
// replace with a real API or mock
console.log('fetching', q, pageNo);
// example: return fetch(`/api/photos?q=${q}&page=${pageNo}`)
}
const gallery = document.getElementById('gallery');
const input = document.getElementById('search');
const debouncedSearch = debounce((value) => {
query = value;
page = 1;
fetchPhotos(query, page);
gallery.innerHTML = ''; // reset gallery
}, 400);
input.addEventListener('input', (e) => debouncedSearch(e.target.value));
const handleScroll = throttle(() => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 200) {
page++;
fetchPhotos(query, page);
}
}, 250);
window.addEventListener('scroll', handleScroll);
// initial load
fetchPhotos();
Why this shows the difference:
- Typing triggers many
input
events; debounce collapses them into one final fetch. - Scrolling fires many events; throttle ensures we load more photos at most once per interval.
⚛️ React: same idea but modular
useDebounce hook
// useDebounce.js
import { useEffect, useState } from 'react';
export default function useDebounce(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const t = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(t);
}, [value, delay]);
return debounced;
}
useThrottleRef for scroll
// useThrottleRef.js
import { useEffect, useRef } from 'react';
export default function useThrottleRef(callback, wait = 200) {
const lastRef = useRef(0);
const cbRef = useRef(callback);
cbRef.current = callback;
useEffect(() => {
function handler() {
const now = Date.now();
if (now - lastRef.current >= wait) {
lastRef.current = now;
cbRef.current();
}
}
window.addEventListener('scroll', handler);
return () => window.removeEventListener('scroll', handler);
}, [wait]);
}
PhotoSearch component (sketch)
import React, { useState, useEffect } from 'react';
import useDebounce from './useDebounce';
import useThrottleRef from './useThrottleRef';
function PhotoSearch() {
const [q, setQ] = useState('');
const debouncedQ = useDebounce(q, 400);
const [page, setPage] = useState(1);
useEffect(() => {
// call API with debouncedQ
// reset page if debouncedQ changed
setPage(1);
// fetchPhotos(debouncedQ, 1)
}, [debouncedQ]);
useThrottleRef(() => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 200) {
setPage((p) => p + 1);
// fetchPhotos(debouncedQ, page + 1)
}
}, 250);
return (
<>
<input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search photos..." />
<div className="gallery">{/* map photos */}</div>
</>
);
}
⚠️ Common beginner mistakes — focused checklist
- Don’t debounce when you need immediate feedback (e.g., toggles).
- Don’t throttle when you need the final value (e.g., final form submit) — or add a trailing call.
- Don’t recreate handlers every render — use hooks or memoised helpers.
- Always remove event listeners in cleanup (React
useEffect
return). - Test
wait
on real devices — 100ms ≠ 100ms on mobile.
🎯 Interview-friendly explanation (short & demo)
30s answer:
Debounce waits for a pause and runs the last event — good for inputs. Throttle runs at most once per interval — good for continuous events like scroll/resize.
Show: paste both helpers side-by-side and explain setTimeout/clearTimeout
vs Date.now()
timestamp checks.
Follow-ups to expect: leading vs trailing behavior, combining throttle+trailing, SSR concerns, handler identity in React.
Mini challenge: Implement throttle(fn, wait, { leading, trailing })
and explain the timeline.
🧠 Key takeaways (one-liner checklist)
- final-value → debounce (search, autosave).
- steady updates → throttle (scroll, resize, telemetry).
- combined needs → consider throttle with trailing or a hybrid.
- test
wait
on real devices (100–300ms typical). - in React, use hooks and
useRef
to keep handlers stable.
👋 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)