

Junior Alves
Senior Developer
Foto: Unsplash
19 de maio de 2025 • 7 minutos de leitura
Deixe sua UI finalmente "respirar"
A técnica essencial para UI fluidas
O Caos
Você já notou como algumas ações em sites ou aplicativos parecem lentas, travam ou demoram a responder quando você digita rápido, rola a página ou redimensiona a janela?
Por trás dessa má UX, muitas vezes, está um problema comum no frontend: eventos que disparam rápido demais!
Pense em eventos como:
- Digitar em um campo de busca (
input
,keyup
) - Redimensionar a janela do navegador (
resize
) - Rolar a página (
scroll
) - Mover o mouse pela tela (
mousemove
)
Esses eventos podem disparar sua função associada dezenas ou até centenas de vezes em um único segundo!
Se a função que você conectou a eles faz algo "pesado" (como uma requisição para o servidor ou um cálculo complexo que altera a interface), o que acontece?
- Lentidão da aplicação: O navegador fica sobrecarregado tentando executar a mesma tarefa repetidamente.
- Sobrecarga no servidor: Múltiplas requisições desnecessárias inundam o backend.
- UI lenta ou travada: A thread principal do JavaScript pode ficar bloqueada, impedindo que a interface do usuário responda.
- Péssima UX: O resultado final é uma aplicação que parece quebrada, lenta e frustrante de usar.
É um verdadeiro caos de eventos tentando passar por um funil pequeno demais para processá-los todos eficientemente. Precisamos de uma forma de controlar essa enxurrada!
Conheça o Debounce
Felizmente temos ferramentas para domar esse caos. Uma das mais elegantes e eficazes é o Debounce.
Em sua essência, o Debounce é uma técnica para limitar a frequência com que uma função é executada.
Ele não dispara a função imediatamente a cada evento. Em vez disso, ele espera um determinado período de inatividade do evento antes de finalmente executar a função associada.
Imagine o cenário: um porteiro só abre a porta depois que a campainha para de tocar por alguns segundos. Se alguém tocar a campainha novamente antes desse tempo, o porteiro "reinicia" sua contagem de espera.
A porta só abre quando há um período de silêncio na campainha.
O Debounce faz exatamente isso: adiciona um tempo de espera e, se o evento disparar novamente durante esse tempo, o timer é resetado.
Isso garante que a função que você quer executar será invocada somente uma única vez depois que esse timer finalizar com sucesso (ou seja, após o evento parar de acontecer pelo tempo determinado).
Ele dá aquele "respiro" crucial para a aplicação processar os eventos de forma mais eficiente, consolidando múltiplos disparos rápidos em uma única ação controlada.
Conceitos importantes
Para construir o Debounce, utilizamos alguns conceitos do JavaScript.
Primeiro, ele é uma função de ordem superior (higher-order function
), agindo como uma "fábrica" que retorna uma nova função, a qual usaremos no lugar da original.
Essa função interna se beneficia das closures
, uma característica da linguagem que permite que ela "lembre" e acesse variáveis (como o ID do timer, a função original e o delay) do escopo externo, mesmo após a função fábrica ter terminado sua execução (se ficou confuso(a), calma, no código vai ficar bem mais claro 😊).
Além disso, dependemos dos mecanismos de temporização do navegador: setTimeout
para agendar a execução futura e clearTimeout
para cancelar agendamentos pendentes, guardando a referência do timer em uma variável.
Por fim, precisamos garantir que a função original, ao ser executada, mantenha o contexto correto (this
) e receba todos os argumentos
, algo que podemos resolver com métodos como apply
ou call
.
O Código em Ação
Unindo todos esses conceitos - a fábrica de funções, closures, o timer que reseta, a chamada com this
e args
- chegamos à implementação da nossa função debounce
:
function debounce(fn, delay) {
let timerId = null; // Declara a variável do timer no escopo da closure
return function(...args) { // A função retornada, que lida com o evento
const context = this; // Captura o 'this' da execução atual
// Se um timer já existe, limpa ele para cancelar a execução anterior
if (timerId) {
clearTimeout(timerId);
}
// Configura um novo timer para executar a função original
timerId = setTimeout(() => {
// Executa a função original usando apply para manter o contexto 'this' e passar os argumentos
fn.apply(context, args);
}, delay);
};
}
Como Isso Funciona?
Como o Debounce consegue essa proeza de esperar a calma? Bora ver!
A função debounce
que criamos não executa a sua função original (fn
) diretamente. Em vez disso, ela é uma "fábrica" de funções.
Ela recebe a sua fn
e o tempo de espera (delay
) e retorna uma NOVA FUNÇÃO.
É essa nova função "debounced" que você conecta ao seu evento (como element.addEventListener('keyup', suaFuncaoDebounced);
).
O "coração" da lógica de Debounce reside dentro dessa nova função retornada. Cada vez que essa função é chamada pelo evento (a cada tecla digitada, por exemplo), ela faz duas coisas essenciais:
- Cancela o Timer Anterior: Ela verifica se já existe um timer agendado de uma chamada anterior e, se existir, o cancela (
clearTimeout
). Isso impede que a execução agendada anteriormente aconteça. - Agenda um Novo Timer: Imediatamente após cancelar o antigo (ou se não havia um), ela agenda um novo timer (
setTimeout
) para executar a função original (fn
) depois dodelay
especificado.
Graças ao conceito de closures em JavaScript, a nova função retornada "lembra" e tem acesso à variável timerId
declarada na função debounce
externa, mesmo depois que a debounce
inicial já terminou sua execução. Isso permite que ela verifique e cancele o timer correto a cada vez.
Além disso, para que a função original (fn
) funcione corretamente quando finalmente for chamada pelo timer, precisamos garantir que ela seja executada com o this
correto (útil em contextos de evento) e que receba quaisquer argumentos que o evento possa ter gerado.
Usamos o método apply()
para fazer essa chamada, passando o this
capturado e os argumentos (args
) que a nova função recebeu.
Exemplo prático no mundo real
Vamos ver um exemplo simples de como usar essa função debounce
em um cenário comum: uma barra de busca que busca dados (aqui vamos apenas simular a busca com um log) um tempo depois que o usuário para de digitar.
<input type="text" id="search-input" placeholder="Digite para buscar...">
// Função que 'simula' a busca - normalmente faria uma requisição fetch/axios aqui
function performSearch(event) {
const query = event.target.value;
console.log("Buscando por:", query);
// Ex: aqui faria fetch(`/api/search?q=${query}`)
}
// Cria a versão 'debounced' da função de busca, com 500ms de espera
const debouncedSearch = debounce(performSearch, 500);
// Conecta a função debounced ao evento 'input' do campo de busca
const searchInput = document.getElementById('search-input');
searchInput.addEventListener('input', debouncedSearch);
console.log("Comece a digitar no campo acima. A busca só acontecerá 500ms depois que você parar de digitar!");
Neste exemplo, não importa o quão rápido o usuário digite, a função performSearch
só será chamada 500 milissegundos depois da última tecla digitada. Isso evita disparar buscas intermediárias desnecessárias e melhora muito a performance!
Conclusão
Dominar técnicas como o Debounce é fundamental para construir interfaces web performáticas e responsivas.
Você não só aprendeu o que ele faz e por que usá-lo, mas também desvendou como ele funciona por dentro ao construir sua própria versão.
Agora você tem mais uma ferramenta poderosa no seu cinto de utilidades para domar o caos dos eventos rápidos!
Ahhh, e se você se perguntou sobre o Throttle, vamos falar sobre ele em breve hehe.
PS.: Sim, foi eu quem fez as ilustrações desse post 😃 (da uma olhada aqui)
Muito obrigado por ter lido até aqui. 👊
Te vejo no próximo post! 🚀
Curtiu? Compartilhe esse post: