Debounce & Throttle

· Padrão Frontend
Juan Kalleo
Juan Kalleo
Senior Fullstack
This page hasn't been translated to English yet — showing the original Portuguese content.

Técnicas para limitar a frequência de execução de uma função (como chamadas de API disparadas por eventos de scroll ou digitação). São parecidas mas resolvem problemas diferentes: debounce espera um período de silêncio antes de executar (útil pra "usuário parou de digitar"), throttle garante um intervalo mínimo entre execuções, mesmo que o evento continue disparando sem parar (útil pra scroll/resize, que disparam dezenas de vezes por segundo).

Debounce

É o padrão de todo campo de busca — o componente de busca compartilhado (shared/ui/filtros/search-input.tsx, ver Busca) guarda o que o usuário está digitando num estado local (rascunho) e só propaga pro filtro real (e dispara a query) depois de 350ms sem nova tecla:

const [rascunho, setRascunho] = useState(valor);

// sincroniza de volta se o filtro mudar por fora (ex.: botão "limpar filtros")
useEffect(() => {
  const frame = window.requestAnimationFrame(() => setRascunho(valor));
  return () => window.cancelAnimationFrame(frame);
}, [valor]);

// só propaga pro filtro de verdade depois de 350ms sem digitar
useEffect(() => {
  if (rascunho === valor) return;
  const timeout = window.setTimeout(() => onChange(rascunho), 350);
  return () => window.clearTimeout(timeout);
}, [rascunho]);

Usamos porque um campo de busca sem debounce dispararia uma requisição por tecla — 350ms é o equilíbrio entre "sentir responsivo" e "não spammar a API". Filtro por <select> não precisa disso (o disparo já é discreto, por onChange, não por tecla).

Throttle

Faz sentido pra handler de evento de alta frequência — scroll, resize, mousemove — onde debounce (esperar silêncio) não serve, porque o objetivo é reagir durante o evento contínuo, só que num ritmo controlado, não a cada disparo. Implementação equivalente à do debounce acima, trocando a lógica do timeout: em vez de resetar o timer a cada chamada, ignora chamada nova enquanto o intervalo mínimo não passou.

function throttle<T extends (...args: unknown[]) => void>(fn: T, intervaloMs: number) {
  let bloqueado = false;
  return (...args: Parameters<T>) => {
    if (bloqueado) return;
    fn(...args);
    bloqueado = true;
    setTimeout(() => { bloqueado = false; }, intervaloMs);
  };
}

Pra um caso pontual, essa função de ~8 linhas resolve sem precisar trazer lodash.throttle como dependência nova.