Debounce e throttle: eventos sem travar a tela

Entenda a diferença entre debounce e throttle, quando usar cada um em busca com digitação, scroll e resize, e como implementar os dois em poucas linhas de JavaScript.


Alguns eventos do navegador disparam sem parar: digitar num campo, rolar a página, redimensionar a janela. Se cada disparo faz uma requisição ou um cálculo pesado, a tela trava e o servidor sofre. Debounce e throttle são as duas técnicas pra domar isso, e a confusão entre elas é muito comum.

O problema

Imagina uma busca que consulta a API a cada tecla:

input.addEventListener("input", (e) => {
  buscar(e.target.value);
});

Quem digita "notebook" dispara 8 requisições em um segundo, e só a última importa. As anteriores gastam rede, servidor e ainda podem chegar fora de ordem, mostrando o resultado de "noteb" depois do de "notebook".

Debounce: espere o usuário parar

Debounce adia a execução até que passe um tempo sem novos eventos. Cada evento novo reinicia a contagem. Só quando o silêncio dura o suficiente, a função roda, uma única vez.

function debounce(fn, atraso) {
  let timer;
 
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), atraso);
  };
}
 
const buscarComDebounce = debounce(buscar, 400);
 
input.addEventListener("input", (e) => {
  buscarComDebounce(e.target.value);
});

Repare que isso só funciona por causa de uma closure: timer fica guardado entre as chamadas. Agora, digitando "notebook" de uma vez, só uma requisição sai, 400 ms depois da última tecla.

Use quando só o resultado final importa: campo de busca, validação de formulário enquanto digita, salvar rascunho automático.

Throttle: no máximo uma vez por intervalo

Throttle garante que a função rode no máximo uma vez a cada intervalo, não importa quantos eventos cheguem. Ele não espera parar, ele limita a cadência.

function throttle(fn, intervalo) {
  let ultimaExecucao = 0;
 
  return (...args) => {
    const agora = Date.now();
    if (agora - ultimaExecucao >= intervalo) {
      ultimaExecucao = agora;
      fn(...args);
    }
  };
}
 
window.addEventListener(
  "scroll",
  throttle(() => atualizarBarraDeProgresso(), 100)
);

Durante uma rolagem contínua, a barra de progresso atualiza a cada 100 ms, em vez de centenas de vezes por segundo.

Use quando você precisa de atualizações contínuas, mas num ritmo controlado: scroll, resize, movimento do mouse, arrastar elementos.

A diferença, lado a lado

Pense num elevador com porta. Debounce é o porteiro que só fecha a porta quando ninguém mais entra há alguns segundos. Throttle é o elevador que sai a cada 10 minutos, tenha chegado muita gente ou pouca.

DebounceThrottle
Quando executaDepois que os eventos paramNo máximo a cada intervalo
Durante a rajadaNada aconteceExecuta periodicamente
Ideal praBusca, validaçãoScroll, resize, drag

Em React

No React, o cuidado é não recriar a função debounced a cada renderização, senão o timer é perdido. Um hook resolve:

import { useEffect, useState } from "react";
 
function useDebounce(valor, atraso) {
  const [debounced, setDebounced] = useState(valor);
 
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(valor), atraso);
    return () => clearTimeout(timer);
  }, [valor, atraso]);
 
  return debounced;
}
 
function Busca() {
  const [texto, setTexto] = useState("");
  const termo = useDebounce(texto, 400);
 
  useEffect(() => {
    if (termo) buscar(termo);
  }, [termo]);
 
  return <input value={texto} onChange={(e) => setTexto(e.target.value)} />;
}

O campo responde na hora (o estado texto muda a cada tecla), mas a busca só reage a termo, que muda depois da pausa. O return () => clearTimeout(timer) limpa o timer anterior, que é exatamente o reinício do debounce.

Cuidados

  • Escolha o atraso com critério. Debounce de 300 a 500 ms costuma ser bom pra busca. Muito alto parece lento, muito baixo não ajuda.
  • Debounce não substitui cancelar requisições. Mesmo com ele, uma resposta antiga pode chegar depois da nova. Use AbortController pra cancelar a anterior.
  • Em scroll, considere alternativas. Para detectar elementos entrando na tela, IntersectionObserver é mais eficiente que ouvir scroll com throttle.
  • Bibliotecas existem. O lodash tem debounce e throttle prontos, com opções extras como executar no início da rajada.

Fechando

Debounce espera o silêncio, throttle impõe um ritmo. Se só o resultado final interessa, debounce. Se o acompanhamento contínuo importa, throttle. Com uma closure e um setTimeout, você evita centenas de requisições desnecessárias e uma tela que engasga.

Direto na sua
caixa de entrada.

Um aviso por e-mail sempre que eu publicar um post novo. Sem spam, sem newsletter chata, só isso.