JavaScript Debounce e Throttle
Aprenda a limitar a frequência de funções em JavaScript com debounce e throttle — o que cada um faz, como implementá-los e quando usar cada um.
Alguns eventos disparam com muito mais frequência do que você consegue responder de forma útil. Digitar em uma caixa de pesquisa aciona um evento input a cada tecla pressionada; rolar uma página pode emitir centenas de eventos scroll por segundo; resize e mousemove são igualmente verbosos. Se cada evento executa um trabalho custoso — uma requisição de rede, um cálculo de layout, uma re-renderização — seu aplicativo trava. Debounce e throttle são dois pequenos wrappers que limitam a frequência com que uma função é executada, mantendo a responsividade alta sem alterar o que a função faz.
Ambos são padrões clássicos de decorator: recebem uma função e retornam uma nova função com o mesmo comportamento mais uma regra de limitação de taxa. São construídos sobre closures para manter estado entre chamadas e sobre temporizadores como setTimeout para adiar ou controlar a execução.
A Ideia Central
As duas técnicas respondem à mesma pergunta — "com que frequência isso deve ser executado?" — de formas opostas:
- Debounce aguarda uma pausa. Ele adia a chamada até que
Nmilissegundos tenham passado desde a última invocação. Se as chamadas continuam chegando, o temporizador continua sendo redefinido e a função nunca é executada. Pense em: "aguardar o silêncio." - Throttle impõe um ritmo constante. Ele permite que a função seja executada no máximo uma vez a cada
Nmilissegundos, não importa quantas vezes seja chamada no intervalo. Pense em: "batimento cardíaco regular."
| Aspecto | Debounce | Throttle |
|---|---|---|
| Dispara quando | A atividade para por N ms | No máximo uma vez a cada N ms |
| Durante uma rajada | Nada é executado até a rajada terminar | Executa em um intervalo fixo |
| Modelo mental | "Aguardar o silêncio" | "Ritmo constante" |
| Bom para | Pesquisa ao digitar, salvamento automático, resize finalizado | Rastreamento de scroll, drag, scroll infinito |
Debounce
Uma função com debounce cancela qualquer temporizador pendente a cada chamada e agenda um novo. Somente quando as chamadas param por delay milissegundos a função encapsulada de fato é executada.
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}Dois detalhes tornam isso robusto. O wrapper coleta cada argumento com parâmetros rest (...args) e os encaminha, de modo que a função encapsulada receba exatamente o que o chamador passou. E invoca fn com fn.apply(this, args) para que o this original seja preservado — importante quando a função com debounce é um método em um objeto. (Veja call e apply e vinculação de função para entender por que encaminhar this importa.)
Veja em ação. Chamar a função encapsulada repetidamente aciona apenas uma execução real, após a atividade se estabilizar:
Como cada tecla redefine o relógio, debounce é ideal quando você quer reagir após o usuário terminar: pesquisa ao digitar, salvamento automático de um rascunho, validação de um campo quando a digitação para, ou recálculo de layout somente quando um redimensionamento de janela se estabilizou.
Throttle
Uma função com throttle é executada imediatamente e depois ignora chamadas adicionais até que um período de espera termine. Isso garante uma frequência máxima em vez de aguardar uma pausa.
function throttle(fn, limit) {
let inThrottle = false;
return function (...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}O flag inThrottle, mantido na closure, atua como um portão. A primeira chamada passa e o portão fecha; qualquer chamada durante o período de espera é descartada; quando o temporizador dispara, o portão reabre para a próxima chamada.
Throttle é adequado para qualquer coisa que flua continuamente e onde você queira atualizações regulares em vez de cada uma individualmente: rastrear posição de scroll, lidar com mousemove durante um drag, carregar mais conteúdo em scroll infinito, ou limitar a frequência com que você acessa uma API com limite de taxa.
Borda Inicial vs. Borda Final
Há uma escolha de design sutil em ambos os wrappers: a função deve disparar na borda inicial (a primeira chamada, imediatamente) ou na borda final (após o delay/período de espera)?
- O
debounceacima é de borda final: nada acontece até que a atividade pare. Um debounce de borda inicial executaria na primeira chamada e ignoraria o restante. - O
throttleacima é de borda inicial: dispara imediatamente e depois controla. Um throttle de borda final também executaria mais uma vez no final da janela para capturar o valor final.
Esses comportamentos de borda importam na prática — um throttle de borda final no scroll, por exemplo, garante que você não perca a posição final de scroll quando o usuário para.
Para código em produção, prefira uma implementação testada em batalha como _.debounce e _.throttle do lodash. Elas lidam com bordas iniciais e finais, uma API cancel()/flush(), e uma opção maxWait (para que uma função com debounce ainda seja executada eventualmente durante atividade contínua). Entender as versões básicas acima é essencial, mas raramente você precisa criar a sua própria.
Um Exemplo Real com DOM
Conectar debounce a um campo de pesquisa é o caso de uso canônico. Adicionamos um listener (veja manipulação de eventos no DOM) e deixamos o wrapper decidir quando o trabalho realmente é executado:
const input = document.querySelector('#search');
function search(event) {
console.log('Querying API for:', event.target.value);
// fetch(`/api/search?q=${event.target.value}`) ...
}
const debouncedSearch = debounce(search, 400);
input.addEventListener('input', debouncedSearch);Agora a requisição de rede dispara somente quando o usuário pausa por 400 ms, em vez de a cada tecla pressionada — uma caixa de pesquisa que antes disparava uma dúzia de requisições para hello agora dispara uma. Note que o listener recebe o objeto event do DOM e, como nosso wrapper encaminha todos os argumentos, search ainda o recebe intacto.
Temporizadores e listeners mantêm referências, por isso limpe-os quando não forem mais necessários. Em um aplicativo de página única ou componente, remova o listener ao desmontar (por exemplo, ao desmontar o componente) e cancele qualquer temporizador pendente para evitar vazamentos de memória e callbacks sendo acionados em elementos que não existem mais:
input.removeEventListener('input', debouncedSearch);Um debounce de produção normalmente também expõe um método cancel() que chama clearTimeout por você.
Escolhendo Entre Eles
Quando você não tem certeza qual usar, pergunte-se o que importa para você:
- Você se importa apenas com o estado final após uma rajada de atividade (o termo de pesquisa concluído, o tamanho da janela estabilizado)? Use debounce.
- Você quer feedback contínuo e regular durante a atividade (progresso do scroll, posição de um elemento arrastado)? Use throttle.
Ambos são leves, independentes de framework, e se constroem diretamente sobre closures e temporizadores — as mesmas bases por trás das arrow functions capturando this e das ferramentas de agendamento que você já viu.