Deixe sua UI finalmente "respirar"
Profile picture

Junior Alves

Senior Developer

Foto: Unsplash

19 de maio de 20257 minutos de leitura

Deixe sua UI finalmente "respirar"

A técnica essencial para UI fluidas

O Caos

Ilustração do Debounce

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.

Ilustração do Debounce

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:

  1. 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.
  2. 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 do delay 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.

Ilustração do Debounce

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!");

Ilustração do Debounce

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:

Todos os direitos reseverdos © Junior Alves 2025