Gabriel / Ramos

⟨ pintor de pixel ⟩

  • Blog
  • Debounce e Throttle

Debounce e Throttle

O que são, para que servem e como funcionam essas técnicas (ou padrões) parar adiar eventos

7 min. de leitura

Foto por Kai Pilger

Quando estamos escrevendo nossa aplicação e lidando com interações em tela, nem sempre queremos que alguma ação aconteça logo de imediato, assim que um evento é disparado. Principalmente quando essa ação envolve alguma requisição HTTP para um serviço externo.

As técnicas de debounce e throttle servem para adiar algum acontecimento dado um determinado tempo.

#Um caso de uso prático e bem comum

Um cenário bem comum é na busca de produtos dentro de um e-commerce.

Quando você entra em um site para comprar um produto, você vai interagir com alguma barra de busca para encontrar o que está pesquisando. Em muitos cenários, dependendo da palavra ou dos trechos que você escreveu, algumas sugestões de busca aparecem. Bem comum, né?

O que tá acontecendo nos bastidores dessa pesquisa com sugestões é que, muito provavelmente, existe toda uma aplicação com um motor de busca baseada rodando em algum servidor e a aplicação que está rodando no lado do cliente faz uma requisição para essa aplicação com o termo digitado que, por sua vez, responde com algumas sugestões a serem exibidas.

No entanto, se essa ação fosse disparada a cada letra que digitamos na barra de busca, poderíamos ter uma sequência de requisições sendo feitas de forma desnecessária.

Vamos imaginar que assim que uma letra é digitada no campo de busca essa ação ocorra:

  • Se você digitar ps5 essa ação será feita 3 vezes;
  • Se você digita (jogos de ps5) essa ação será feita 12 vezes.

Como nossa ação está sendo disparada a cada letra digitada, a requisição ao servidor será realizada a mesma quantidade de vezes que o usuário teclar na barra de busca.

Além de sobrecarregar nosso serviço com chamadas desnecessariamente, a aplicação que roda no navegador provavelmente irá atualizar várias vezes também com alguns resultados de busca que não são interessantes para o usuário. Ambas as aplicações estão tendo seu desempenho prejudicado desnecessariamente.

Além disso, se você está pesquisando por ps5 é provável que só queira ver produtos relacionado a esse termo, e não produtos relacionados à um termo p ou ps (enquanto digita), não acha?

Essa ação poderia ser adiada usando debounce ou throttle para que essa pesquisa só fosse feita após um certo período de tempo em que o campo de busca foi preenchido, o que é bem melhor pra todo mundo, pra experiência de quem está comprando e para as duas aplicações (no cliente e no servidor)!

#Como funcionam essas técnicas?

Ambas as técnicas funcionam de maneira parecida e isso é bem confuso de entender, mas vamos pensar em um cenário mais interessante: você é uma criança e está viajando com seus pais, vocês estão indo de carro passar as férias 🏝 em algum lugar (deixe sua imaginação fluir aqui).

Como toda criança, você não gosta de ficar esperando e está super ansiosa pra chegar no destino e de 5 em 5 minutos você faz aquela pergunta que todo mundo já vez na vida: "tamo chegando?".

A cada vez que você pergunta, seus pais muito provavelmente vão responder "sim" ou "não" e talvez ignorar você depois de um tempo.

Inevitavelmente vocês vão chegar no destino e ficar perguntando não vai mudar muita coisa, certo?

Se "chegar no destino" fosse uma ação, nesse momento, ela estaria sofrendo throttle. Não importa quantas vezes você pergunte, ela só será realizada quando o trajeto da viagem for concluído e perguntar não vai fazer você chegar mais rápido.

Utilizando throttling, não importa quantas vezes você tente realizar uma ação, ela só será realizada ao final de um determinado tempo.

Agora vamos pensar em um outro cenário, onde seus pais querem zoar você e, a cada vez que você pergunta se está chegando, eles dão uma volta a mais em alguma quadra. Por fim, vocês não vão chegar no destino até que você pare de perguntar. Nesse caso, essa ação estaria sofrendo debounce. Dessa vez, perguntar faz com que a chegada ao destino fique cada vez mais atrasada.

Utilizando debounce, a cada vez que você tenta realizar uma ação, ela será adiada por mais um tempo, até que não haja mais tentativa e ela seja, por fim, executada.

O post no site Telerik, escrito por Rupesh Mishra também tem uma ótima analogia com bolo que pode complementar.

#Um pequeno exemplo com código

#Criando nossa ação principal

Vamos voltar ao nosso primeiro exemplo do e-commerce com nossa barra de busca. Vamos imaginar que nossa simplesmente exibe um log na tela como:

const acao = () => {
  console.log('ação realizada, pesquisando...');
};

Como sabemos que iremos trabalhar para adiar essa ação, vamos criar uma variável de tempo. Iremos adiar sua execução em 3 segundos, portanto, 3000 milissegundos:

const acao = () => {
  console.log('ação realizada, pesquisando...');
};
const tempo = 3000;

#Criando nossas funções "atrasadoras"

Já sabemos que vamos trabalhar com debounce e throttle. Para trabalhar com tempo e adiar alguma execução utilizaremos o setTimeout.

Para deixar as coisas mais informais, vamos chamar essas funções de "atrasadoras", tudo bem?

#Debounce

No caso do debounce precisamos receber uma ação e um determinado tempo como parâmetro e, como essa ação deverá ficar se repetindo, retornaremos uma nova função:

const debounce = (fn, tempo) => {
  return () => {
  };
};

Precisamos ter uma maneira de controlar esse comportamento onde, a cada vez que a função de retorno é chamada, seu tempo é reiniciado e ela só será executada uma única vez ao final de todas as tentativas. Vamos criar uma variável debounced que será usada para isso:

const debounce = (fn, tempo) => {
  let debounced;

  return () => {
  };
};

Iremos executar a função setTimeout passando como argumento nosso parâmetros fn e tempo. Essa nova variável debounced irá armazenar o retorno do temporizador que a função setTimeout retorna:

const debounce = (fn, tempo) => {
  let debounced;

  return () => {
    debounced = setTimeout(fn, tempo);
  };
};

Agora, não podemos esquecer de limpar nosso temporizador a cada vez que uma tentativa de executar a ação fn é realizada (simulando aquela volta extra na quadra a cada vez que você perguntava "estamos chegando?"). Podemos fazer isso com a função clearTimeout:

const debounce = (fn, tempo) => {
  let debounced;

  return () => {
    clearTimeout(debounced);
    debounced = setTimeout(fn, tempo);
  };
};
#Throttle

Já essa função será ainda mais simples.

Como agora a ação será realizada independente de quantas vezes é chamada, só precisaremos usar nosso setTimeout.

const throttle = (fn, tempo) => {
  setTimeout(fn, tempo);
};

Vamos manter o retorno de uma função para que possamos continuar com a "tentativa" de chamá-la (como se fosse a constante pergunta "estamos chegando"):

const throttle = (fn, tempo) => {
  setTimeout(fn, tempo);

  return () => {
    // não precisamos fazer nada
    // mas vamos retornar uma função pra manter
    // o mesmo raciocínio do nosso outro exemplo
  };
};

#Juntando tudo e perguntando "já chegamos?" diversas vezes

Podemos criar mais duas funções e adicionar alguns logs extras, apenas pra deixar nosso exemplo mais claro.

Podemos até colocar mais um setTimeout para mostrar a diferença do debounce e do throttle.

Vamos criar uma função executaAcaoComDebounce que irá tentar executar a nossa acao "atrasada" diversas vezes.

const executaAcaoComDebounce = () => {
  // um log qualquer de início
  console.log('iniciando execução com debounce');

  // criamos nossa função baseada na ação principal com debounce
  const debounced = debounce(acao, tempo);

  // tentamos executar a função varias vezes
  debounced();
  debounced();
  debounced();
  setTimeout(() => {
    // tentamos executar após o tempo de 2 segundos (3 segundos - 1 segundo)
    console.log('timeout tentando chamar a função debounced')
    debounced();
  }, tempo - 1000);
};

Agora vamos fazer o mesmo exemplo, mas chamado executaAcaoComThrottle, fazendo as devidas modificações:

const executaAcaoComThrottling = () => {
  // um log qualquer de início
  console.log('iniciando execução com throttling');

  // criamos nossa função baseada na ação principal com throttle
  const throttled = throttle(acao, tempo);

  // tentamos executar a função varias vezes
  throttled();
  throttled();
  throttled();
  setTimeout(() => {
      // tentamos executar após o tempo de 2 segundos (3 segundos - 1 segundo)
      console.log('timeout tentando chamar a função throttled');
      throttled();
  }, tempo - 1000);
};

Agora, basta executar qualquer uma dessas duas funções e ver o resultado!

Se você notar, como o debounce atrasa a execução da ação, ele só será executado 3 segundos após todas as tentativas terminarem. Já a versão com throttle, será executada após 3 segundos, independente de quantas vezes tentamos e é justamente por isso que nossa mensagem ação realizada, pesquisando... aparecerá antes/depois dependendo do exemplo que você estiver executando.

#Código completo

Para facilitar, o código completo é esse aqui:

// Ação principal
const acao = () => {
  console.log('ação realizada, pesquisando...');
};
const tempo = 3000;

// Funções atrasadoras
const debounce = (fn, tempo) => {
  let debounced;

  return () => {
    clearTimeout(debounced);
    debounced = setTimeout(fn, tempo);
  };
};
const throttle = (fn, tempo) => {
  setTimeout(fn, tempo);

  return () => {};
};

// Ações com tentativas
const executaAcaoComDebounce = () => {
  console.log('iniciando execução com debounce');

  const debounced = debounce(acao, tempo);

  debounced();
  debounced();
  debounced();
  setTimeout(() => {
    console.log('timeout tentando chamar a função debounced')
    debounced();
  }, tempo - 1000);
};

const executaAcaoComThrottling = () => {
  console.log('iniciando execução com throttling');

  const throttled = throttle(acao, tempo);

  throttled();
  throttled();
  throttled();
  setTimeout(() => {
      console.log('timeout tentando chamar a função throttled');
      throttled();
  }, tempo - 1000);
};

Compartilhe