Throttle e Debounce patterns em JavaScript

Throttle e Debounce patterns em JavaScript

Alguns eventos do browser acontecem de forma mais rápida e com mais frequência do que gostaríamos — como os eventos resize e scroll da window. Outras vezes nossos queridos usuários disparam mais eventos do que havíamos previsto em nossa aplicação, como um duplo-clique em um botão de submit de um form  AJAX. Neste post iremos aprender a controlar a frequência de execução de um determinado trecho de código JavaScript, a diferença entre debounce e throttle, quando e porque usá-las e a se defender de situações corriqueiras como as que acabamos de citar.

Throttle

Imaginem o throttle como uma válvula (na verdade essa é a tradução) que regula a quantidade (o fluxo) de vezes que um dado trecho de código será executado durante um determinado espaço de tempo. Com esta técnica podemos garantir que um trecho de código não será executado mais que 1 vez a cada X milisegundos.

Throttle funciona como uma válvula

Um caso de uso comum para o throttle é no controle do disparo dos handlers dos eventos scroll e resize. Normalmente são disparados vários desses eventos em um curto espaço de tempo e, caso estejamos executando qualquer computação quando estes eventos são disparados, provavelmente vamos acabar causando uma lentidão na renderização de nossa aplicação, já que será executado várias vezes o mesmo trecho de código JavaScript e a renderização no browser acontece em apenas uma thread.

Debounce

O debounce, assim como o throttle, limita a quantidade de vezes que um determinado trecho de código é executado em relação ao tempo. Mas diferentemente do throttle — que assegura que aconteçam no máximo 1 execução a cada X milisegundos —, o debounce irá postergar a execução do código caso ele seja invocado novamente em menos de X segundos.

Esta técnica é bastante útil para decidirmos, por exemplo, quando devemos chamar uma função AJAX de um input com autocomplete. Imaginem que estamos fazendo uma consulta à API de autocomplete da nossa aplicação em todo evento keydown do input. Muito provavelmente o usuário dispara eventos keydown mais rápido que o nosso servidor é capaz de entregar para o browser as sugestões de autocomplete. Com isso, corremos o risco de entregar uma sugestão desatualizada ao usuário. Com o debounce, podemos disparar esta mesma função de autocomplete depois de, por exemplo, 300 milisegundos após a última tecla ter sido pressionada. Dessa forma, entregamos uma sugestão atualizada e não sobrecarregamos nosso back-end com consultas desnecessárias.

Debounce posterga a execução

Outro cenário onde é bastante usado o debounce é em botões de submit de formulários. Imaginem que temos um formulário onde o submit acontece via AJAX. Caso o usuário dê um clique duplo no botão, iremos enviar duas requisições ao back-end. Com um debounce do evento de clique, podemos postergar a execução do submit via AJAX para, por exemplo, depois de 200 milisegundos depois do último clique, garantindo assim, por exemplo, que um mesmo item não seja comprado duas vezes sem a intenção do usuário.

Experimento

No experimento abaixo vamos tentar explicar de uma forma mais visual como funciona o throttle e debounce. Nele estamos limitando o disparo do handler do evento mousemove. Cada barra equivale a 200 milisegundos, e uma barra maior significa que o handler foi disparado.

Continuem movendo o mouse dentro do experimento e reparem como os handlers dos eventos com throttle e debounce são disparados de maneiras diferentes.

O handler do evento não tratado é sempre disparado. Na verdade ele é disparado bem mais frequentemente que a cada 200 milisegundos — na minha máquina ele é disparado, em média, a cada 13 milisegundos.

O throttle funciona como uma válvula e não permite que o handler seja executado mais de 1 vez a cada 400 milisegundos (uma vez a cada 2 barras).

O debounce posterga a execução do handler caso ele seja chamado novamente em menos de 200 milisegundos. Você deve parar um pouco de mexer o mouse para parar de postergar o handlerdebounced.

Como usar

Vamos agora ver como aplicar os conceitos de throttle e debounce na prática. Começando pelo throttle.

var onResize = (function () {
  'use strict';

  var timeWindow = 200; // tempo em ms
  var lastExecution = new Date((new Date()).getTime() - timeWindow);

  var onResize = function (args) {
     // nosso código é escrito nessa função
  };

  return function() {
    if ((lastExecution.getTime() + timeWindow) <= (new Date()).getTime()) {
      lastExecution = new Date();
      return onResize.apply(this, arguments);
    }
  };
}());

Calma. Respire. O algoritmo acima não é tão difícil. Na primeira linha, nós definimos que a variável onResize recebe o valor retornado pela função auto-executável — em inglês Immediately-Invoked Function Expression, ou IIFE — declarada após o sinal de =. Não se deixe levar pelos nomes bonitos. Uma função auto-executável é apenas — como o próprio nome diz — uma função que se executa e serve apenas como um escopo para declararmos variáveis “privadas”.

Dentro da nossa IIFE no trecho var onResize = function definimos a lógica que queremos que seja executada. Na variável timeWindow o tempo minímo entre as execuções do trecho de código throttled. E, no final, retornamos a função onResize — com seu contexto devidamente setado — apenas se a última chamada a ela foi em menos de timeWindow milisegundos.

Agora vamos estudar o debounce. Como ele posterga a execução de um dado trecho de código, vamos brincar bastante com o setTimeout.

var autocomplete = (function () {
  'use strict';

  var timeWindow = 500; // tempo em ms
  var timeout;

  var autocomplete = function (arg1, arg2) {
    // nossa lógica aqui
  };

  return function() {
    var context = this;
    var args = arguments;
    clearTimeout(timeout);
    timeout = setTimeout(function(){
      autocomplete.apply(context, args);
    }, timeWindow);
  };
}());

Este exemplo utiliza, da mesma forma que o exemplo do throttle, uma IIFE. É ela que irá retornar a função debounced. Dentro dela setamos na variável timeWindow a janela de tempo em que, caso nossa função seja novamente chamada, iremos postergar sua execução.

No retorno de nossa IIFE que começa nosso jogo com o setTimeout. Toda vez que nossa função autoComplete  debounced for chamada, o que acontece é que nós limpamos qualquer setTimeout antigo e setamos um novo. Então, se ficarmos sempre a chamando, iremos sempre limpar o timeout que a iria disparar e setamos um novo.

E como estamos usando uma IIFE, o que está dentro dela, como variáveis e funções, só é visível pela nossa função.

Caso não queiram quebrar tanto a cabeça entendendo os algoritmos de implementação, algumas bibliotecas JavaScript, como o underscore.js, já os trazem implementados prontos para aplicarmos throttle e debounce em funções já existentes.

// função que será disparada no evento "onresize" da window
function onResizeHandler() {
  // ...
}

// throttle com underscore.js
// garante que a função "onResizeHandler" não será executada
// mais que uma vez a cada 200ms
$(window).on('resize', _.throttle(onResizeHandler, 200));

E para quem usa o Sublime Text, eu fiz um package com vários patterns de JavaScript, inclusive com o Throttle e Debounce, que já está disponível no Package Manager. Basta procurar por JavaScript Patterns.

#53