DEV Community

SAURAV KUMAR
SAURAV KUMAR

Posted on

⏱️ Debounce vs Throttle — How to Choose & Build One Mini Project

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);
  };
}

Enter fullscreen mode Exit fullscreen mode

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);
    }
  };
}

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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();

Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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]);
}

Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

⚠️ 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)