setTimeout, setInterval e debounce/throttle
Esta aula explora os mecanismos de agendamento e cancelamento de tarefas em JavaScript, com foco em setTimeout e setInterval, e apresenta as técnicas de debounce e throttle para controlar a frequência de execução de funções em eventos de alta taxa, como scroll e digitação.
Bem-vindo à aula 47 do nosso curso de JavaScript! Hoje vamos mergulhar em um dos aspectos mais poderosos e, ao mesmo tempo, mais sutis da linguagem: o agendamento de tarefas. Você já deve ter usado setTimeout ou setInterval em algum momento, mas talvez não tenha explorado todas as suas nuances, como o comportamento assíncrono, o cancelamento e as armadilhas comuns. Além disso, vamos apresentar duas técnicas essenciais para otimizar o desempenho em aplicações reais: debounce e throttle. Essas técnicas são fundamentais para lidar com eventos que disparam muitas vezes por segundo, como rolagem de página, redimensionamento de janela ou digitação em um campo de busca. Ao final desta aula, você terá um domínio sólido sobre como controlar o tempo de execução de funções, evitando problemas de performance e criando experiências de usuário mais fluidas.
Vamos começar entendendo o modelo de concorrência do JavaScript, que é single-threaded e baseado em eventos. Isso significa que não podemos simplesmente "pausar" o código; em vez disso, usamos temporizadores para agendar a execução de funções em momentos futuros. Esses temporizadores são gerenciados pelo ambiente (navegador ou Node.js) e colocados na fila de eventos. Vamos explorar como isso funciona na prática.
Agendamento
O agendamento de tarefas em JavaScript é feito principalmente por duas funções: setTimeout e setInterval. Ambas são fornecidas pelo ambiente (navegador ou Node.js) e permitem que você execute uma função após um determinado atraso ou em intervalos regulares. Vamos detalhar cada uma.
setTimeout executa uma função uma única vez após um atraso especificado (em milissegundos). Sua sintaxe é:
const timeoutId = setTimeout(funcao, atraso, ...argumentos);Onde funcao é a função a ser executada, atraso é o tempo em milissegundos, e ...argumentos são argumentos opcionais que serão passados para a função quando ela for chamada. É importante notar que o atraso não é garantido: ele representa o tempo mínimo antes da execução, pois a função só será executada quando o event loop estiver livre. Por exemplo, se você agendar um setTimeout com 0 ms, ele não executa imediatamente; ele será executado assim que a pilha de chamadas atual for esvaziada.
Vejamos um exemplo simples:
console.log("Início");
setTimeout(() => {
console.log("Executado após 2 segundos");
}, 2000);
console.log("Fim");
// Saída: Início, Fim, Executado após 2 segundosNote que a ordem é preservada: o código síncrono é executado primeiro, e a função do temporizador é executada depois, mesmo que o atraso seja zero. Isso é um comportamento fundamental do JavaScript.
setInterval executa uma função repetidamente em intervalos fixos. Sua sintaxe é semelhante:
const intervalId = setInterval(funcao, intervalo, ...argumentos);O intervalo é o tempo entre cada execução. No entanto, é preciso ter cuidado: se a função demorar mais que o intervalo, o navegador pode encadear execuções, causando problemas de desempenho. Além disso, o intervalo não é preciso: ele é afetado pela carga do event loop e por outros fatores.
Um exemplo de uso de setInterval para criar um relógio:
const relogio = setInterval(() => {
const agora = new Date();
console.log(agora.toLocaleTimeString());
}, 1000);
// Para parar, use clearInterval(relogio);É importante entender que setTimeout e setInterval retornam um ID numérico que pode ser usado para cancelar o agendamento. Vamos ver isso na próxima seção.
Cancelamento
Em muitos cenários, você precisará cancelar um temporizador antes que ele seja executado. Para isso, usamos clearTimeout e clearInterval, que recebem o ID retornado pelo respectivo agendamento.
clearTimeout cancela um temporizador criado por setTimeout. Exemplo:
const timeoutId = setTimeout(() => {
console.log("Esta mensagem nunca será exibida");
}, 5000);
// Cancelar antes do tempo
clearTimeout(timeoutId);Da mesma forma, clearInterval cancela um intervalo:
let contador = 0;
const intervalId = setInterval(() => {
contador++;
console.log(`Contador: ${contador}`);
if (contador === 5) {
clearInterval(intervalId);
}
}, 1000);Nesse exemplo, o intervalo é interrompido após cinco execuções. É uma prática comum usar essa técnica para controlar loops baseados em tempo.
Uma armadilha comum é tentar cancelar um temporizador que já foi executado: isso não causa erro, mas é inútil. Além disso, em componentes de front-end, é essencial limpar temporizadores ao desmontar o componente para evitar vazamentos de memória. Por exemplo, em React, você usaria useEffect com cleanup.
Vamos ver um exemplo mais realista: um botão que desabilita por alguns segundos após o clique, para evitar múltiplos envios de formulário.
const botao = document.getElementById("enviar");
botao.addEventListener("click", function() {
botao.disabled = true;
console.log("Enviando...");
setTimeout(() => {
botao.disabled = false;
console.log("Pronto para novo envio");
}, 3000);
});Aqui, o setTimeout é usado para reabilitar o botão após 3 segundos. Se o usuário clicar novamente antes, o botão está desabilitado, e se o componente for destruído, devemos limpar o timer.
Debounce e throttle na prática
Quando lidamos com eventos que disparam em alta frequência (como scroll, resize, keyup em um campo de busca), executar uma função a cada evento pode sobrecarregar o navegador e causar lentidão. Para resolver isso, usamos duas técnicas: debounce e throttle.
Debounce garante que uma função só seja executada após um período de inatividade. Ou seja, se o evento for disparado repetidamente, o temporizador é reiniciado a cada disparo, e a função só é chamada quando o usuário "pausa" por um determinado tempo. Isso é útil para busca autocompletar: você não quer fazer uma requisição a cada tecla, mas sim quando o usuário para de digitar por 300 ms, por exemplo.
Uma implementação simples de debounce:
function debounce(fn, atraso) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, atraso);
};
}
// Uso:
const busca = debounce((termo) => {
console.log("Buscando:", termo);
// requisição à API
}, 300);
campoBusca.addEventListener("input", (event) => {
busca(event.target.value);
});Nessa implementação, cada nova chamada reinicia o temporizador. Se o usuário digitar "a", "ab", "abc" rapidamente, apenas a última chamada (após 300 ms de pausa) será executada.
Throttle limita a taxa de execução: a função é chamada no máximo uma vez a cada intervalo de tempo, independentemente de quantas vezes o evento ocorrer. Isso é útil para eventos de scroll, onde você quer atualizar a interface, mas não a cada pixel.
Uma implementação simples de throttle usando timestamp:
function throttle(fn, limite) {
let ultimaChamada = 0;
return function(...args) {
const agora = Date.now();
if (agora - ultimaChamada >= limite) {
fn.apply(this, args);
ultimaChamada = agora;
}
};
}
// Uso:
const onScroll = throttle(() => {
console.log("Scroll executado");
// atualizar posição
}, 200);
window.addEventListener("scroll", onScroll);Aqui, a função só será executada no máximo a cada 200 ms, mesmo que o evento de scroll dispare muitas vezes.
Existem variações e bibliotecas como Lodash que oferecem implementações robustas com opções de leading/trailing. Mas é importante entender o conceito e ser capaz de implementar a versão simples.
Vamos comparar os dois em um exemplo prático: um campo de busca que faz sugestões. Com debounce, você espera o usuário parar de digitar; com throttle, você pode fazer uma chamada a cada 500 ms enquanto digita, mas não mais que isso. A escolha depende do caso de uso: debounce é melhor para evitar chamadas desnecessárias; throttle é melhor para manter uma taxa constante de atualização.
Boas práticas e observações finais
Ao trabalhar com temporizadores, lembre-se de:
- Sempre limpar temporizadores quando não forem mais necessários, especialmente em componentes de UI.
- Cuidado com o uso de
setIntervalem aplicações de longa duração; prefirasetTimeoutrecursivo se a tarefa puder variar em duração. - Use
requestAnimationFramepara animações, pois ele é sincronizado com a taxa de atualização do monitor e é mais eficiente. - Teste o comportamento do event loop: lembre-se de que o atraso não é preciso.
Referências
- MDN: setTimeout
- MDN: setInterval
- MDN: clearTimeout
- MDN: clearInterval
- Lodash: debounce
- Lodash: throttle
- MDN: requestAnimationFrame
- Node.js: Timers
- JavaScript.info: setTimeout e setInterval
- CSS-Tricks: Debouncing e Throttling
Exercícios
- Exercício 1: Crie uma função
esperarque retorna uma Promise que resolve após um atraso dado (em ms). UsesetTimeoutinternamente. Teste comasync/await.
function esperar(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
(async () => {
console.log("Início");
await esperar(2000);
console.log("Fim após 2s");
})();mostrarMensagem que exibe uma mensagem no console e depois a limpa após 3 segundos, usando setTimeout. Garanta que, se chamada novamente antes dos 3 segundos, o temporizador anterior seja cancelado.let timer = null;
function mostrarMensagem(msg) {
if (timer) clearTimeout(timer);
console.log(msg);
timer = setTimeout(() => {
console.log("Mensagem limpa");
timer = null;
}, 3000);
}
mostrarMensagem("Olá");
// Se chamar mostrarMensagem("Mundo") antes de 3s, o primeiro timer é cancelado.setInterval. Pare-o após 10 segundos. Use clearInterval.let segundos = 0;
const relogio = setInterval(() => {
segundos++;
console.log(`Tempo: ${segundos}s`);
if (segundos === 10) {
clearInterval(relogio);
console.log("Relógio parado");
}
}, 1000);debounce que aceita uma função e um atraso, e retorna uma versão com debounce. Teste com um evento de input simulado.function debounce(fn, atraso) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, atraso);
};
}
// Teste:
const log = debounce((msg) => console.log(msg), 300);
log("a");
log("b");
log("c"); // apenas "c" aparecerá após 300msthrottle que limita a execução a no máximo uma vez por intervalo. Teste chamando várias vezes em sequência.function throttle(fn, limite) {
let ultimaChamada = 0;
return function(...args) {
const agora = Date.now();
if (agora - ultimaChamada >= limite) {
fn.apply(this, args);
ultimaChamada = agora;
}
};
}
const fn = throttle((msg) => console.log(msg), 1000);
fn("primeira"); // executa
fn("segunda"); // ignorada (menos de 1s depois)
setTimeout(() => fn("terceira"), 1100); // executa após 1.1s