Até agora todo o nosso código executou imediatamente — uma linha após a outra, sem pausas. Mas aplicações reais precisam de tempo. Um toast que some após 3 segundos. Um carrossel que avança automaticamente. Um cronômetro. Um salvamento automático. Uma animação que ocorre em etapas.
Para isso existem os temporizadores — setTimeout e setInterval. São duas das funções mais usadas no JavaScript do navegador e, apesar de simples, escondem alguns comportamentos que todo desenvolvedor precisa entender.
setTimeout — executar uma vez após um delay
setTimeout agenda a execução de uma função após um tempo determinado (em milissegundos):
// Sintaxe: setTimeout(função, delay, ...argumentos)
setTimeout(() => {
console.log("Isso executou após 2 segundos!");
}, 2000);
console.log("Esta linha executa imediatamente.");
// Saída:
// "Esta linha executa imediatamente."
// (2 segundos depois...)
// "Isso executou após 2 segundos!"
Note que o código não para enquanto espera — o JavaScript continua executando e o callback é chamado depois. Isso é a natureza assíncrona do JavaScript, que estudaremos em profundidade no Módulo 3.
Passando argumentos para o callback
function saudar(nome, periodo) {
console.log(`Bom ${periodo}, ${nome}!`);
}
// Os argumentos extras são passados para a função
setTimeout(saudar, 1000, "Ana", "dia");
// Após 1 segundo: "Bom dia, Ana!"
Cancelando um setTimeout
setTimeout retorna um ID que você pode usar para cancelar o agendamento antes que ele execute:
const idTimer = setTimeout(() => {
console.log("Este nunca vai executar.");
}, 5000);
// Cancela antes dos 5 segundos
clearTimeout(idTimer);
console.log("Timer cancelado!");
Isso é muito útil em validações com delay — como o salvamento automático que vimos no artigo anterior:
let timerSalvar = null;
input.addEventListener("input", () => {
// Cancela o timer anterior (se existir)
clearTimeout(timerSalvar);
// Agenda novo salvamento após 1 segundo de inatividade
timerSalvar = setTimeout(() => {
salvar(input.value);
}, 1000);
});
Esse padrão — cancelar e reagendar um timer a cada evento — se chama debounce. Voltaremos a ele em detalhes.
setInterval — executar repetidamente
setInterval executa uma função repetidamente em intervalos fixos:
// Executa a cada 1 segundo
const idIntervalo = setInterval(() => {
console.log("Tick! " + new Date().toLocaleTimeString("pt-BR"));
}, 1000);
// Para parar o intervalo:
clearInterval(idIntervalo);
Cancelando um setInterval
Sempre guarde o ID retornado pelo setInterval — você vai precisar dele para parar:
let contador = 0;
const id = setInterval(() => {
contador++;
console.log(`Contagem: ${contador}`);
if (contador >= 5) {
clearInterval(id);
console.log("Intervalo encerrado.");
}
}, 500);
// Contagem: 1
// Contagem: 2
// Contagem: 3
// Contagem: 4
// Contagem: 5
// Intervalo encerrado.
O delay real não é garantido
Um detalhe importante: o delay informado é o tempo mínimo, não exato. O JavaScript é single-threaded — se a thread estiver ocupada, o callback esperará:
setTimeout(() => {
console.log("Deveria executar em 0ms");
}, 0);
// Código pesado que bloqueia a thread por 2 segundos
const inicio = Date.now();
while (Date.now() - inicio < 2000) {}
console.log("Thread livre agora.");
// Saída:
// "Thread livre agora."
// "Deveria executar em 0ms" ← executou depois, apesar do delay 0!
setTimeout(fn, 0) não significa "agora" — significa "assim que a thread estiver livre". Isso é o Event Loop funcionando, que estudaremos no Módulo 3.
Construindo um cronômetro completo
Vamos construir um cronômetro funcional com start, pause e reset:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>Cronômetro</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', sans-serif;
background: #0f1117;
color: #e4e6f0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.cronometro {
text-align: center;
padding: 3rem 4rem;
background: #1a1d27;
border-radius: 20px;
border: 1px solid #2e3248;
box-shadow: 0 20px 60px rgba(0,0,0,.4);
}
h1 {
font-size: 1rem;
font-weight: 500;
color: #8b8fa8;
letter-spacing: .15em;
text-transform: uppercase;
margin-bottom: 2rem;
}
.display {
font-size: 5rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
letter-spacing: .05em;
color: #e4e6f0;
margin-bottom: .5rem;
}
.milissegundos {
font-size: 1.5rem;
color: #7c6ff7;
font-variant-numeric: tabular-nums;
margin-bottom: 2.5rem;
}
.botoes {
display: flex;
gap: 1rem;
justify-content: center;
}
button {
padding: .75rem 2rem;
border: none;
border-radius: 10px;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
transition: all .2s;
letter-spacing: .05em;
}
#btn-start {
background: #7c6ff7;
color: white;
min-width: 120px;
}
#btn-start:hover { background: #6c5fe0; }
#btn-lap {
background: #232637;
color: #e4e6f0;
border: 1px solid #2e3248;
}
#btn-lap:hover { background: #2e3248; }
#btn-reset {
background: #232637;
color: #f87171;
border: 1px solid #2e3248;
}
#btn-reset:hover { background: #2e3248; }
button:disabled {
opacity: .4;
cursor: not-allowed;
}
.voltas {
margin-top: 2rem;
max-height: 200px;
overflow-y: auto;
text-align: left;
}
.volta {
display: flex;
justify-content: space-between;
padding: .5rem 0;
border-bottom: 1px solid #2e3248;
font-size: .9rem;
animation: entrar .2s ease;
}
@keyframes entrar {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
.volta .numero { color: #8b8fa8; }
.volta .tempo { color: #7c6ff7; font-weight: 600; font-variant-numeric: tabular-nums; }
.volta.melhor .tempo { color: #4ade80; }
.volta.pior .tempo { color: #f87171; }
</style>
</head>
<body>
<div class="cronometro">
<h1>⏱ Cronômetro</h1>
<div class="display" id="display">00:00</div>
<div class="milissegundos" id="milissegundos">.000</div>
<div class="botoes">
<button id="btn-start">Iniciar</button>
<button id="btn-lap" disabled>Volta</button>
<button id="btn-reset" disabled>Reset</button>
</div>
<div class="voltas" id="voltas"></div>
</div>
<script>
// ── Estado ──────────────────────────────────────────
let idIntervalo = null;
let iniciou = null;
let totalAcumulado = 0;
let rodando = false;
let voltas = [];
// ── Referências ─────────────────────────────────────
const display = document.querySelector("#display");
const displayMs = document.querySelector("#milissegundos");
const btnStart = document.querySelector("#btn-start");
const btnLap = document.querySelector("#btn-lap");
const btnReset = document.querySelector("#btn-reset");
const listaVoltas = document.querySelector("#voltas");
// ── Formatação ──────────────────────────────────────
function pad(numero, casas = 2) {
return String(numero).padStart(casas, "0");
}
function formatarTempo(ms) {
const minutos = Math.floor(ms / 60000);
const segundos = Math.floor((ms % 60000) / 1000);
const milissegundos = ms % 1000;
return {
principal: `${pad(minutos)}:${pad(segundos)}`,
ms: `.${pad(milissegundos, 3)}`,
};
}
// ── Atualização do display ──────────────────────────
function atualizar() {
const agora = Date.now();
const decorrido = totalAcumulado + (agora - iniciou);
const { principal, ms } = formatarTempo(decorrido);
display.textContent = principal;
displayMs.textContent = ms;
}
// ── Ações ────────────────────────────────────────────
function iniciar() {
iniciou = Date.now();
rodando = true;
// Atualiza a cada 10ms para mostrar milissegundos fluindo
idIntervalo = setInterval(atualizar, 10);
btnStart.textContent = "Pausar";
btnLap.disabled = false;
btnReset.disabled = false;
}
function pausar() {
clearInterval(idIntervalo);
totalAcumulado += Date.now() - iniciou;
rodando = false;
btnStart.textContent = "Continuar";
btnLap.disabled = true;
}
function reset() {
clearInterval(idIntervalo);
idIntervalo = null;
rodando = false;
totalAcumulado = 0;
iniciou = null;
voltas = [];
display.textContent = "00:00";
displayMs.textContent = ".000";
listaVoltas.innerHTML = "";
btnStart.textContent = "Iniciar";
btnLap.disabled = true;
btnReset.disabled = true;
}
function registrarVolta() {
const agora = Date.now();
const tempoTotal = totalAcumulado + (agora - iniciou);
const tempoVolta = voltas.length === 0
? tempoTotal
: tempoTotal - voltas.reduce((acc, v) => acc + v.duracao, 0);
voltas.push({ numero: voltas.length + 1, duracao: tempoVolta });
renderizarVoltas();
}
function renderizarVoltas() {
listaVoltas.innerHTML = "";
if (voltas.length === 0) return;
const duracoes = voltas.map(v => v.duracao);
const melhor = Math.min(...duracoes);
const pior = Math.max(...duracoes);
// Exibe do mais recente ao mais antigo
[...voltas].reverse().forEach(volta => {
const div = document.createElement("div");
div.classList.add("volta");
if (voltas.length > 1) {
if (volta.duracao === melhor) div.classList.add("melhor");
if (volta.duracao === pior) div.classList.add("pior");
}
const { principal, ms } = formatarTempo(volta.duracao);
div.innerHTML = `
<span class="numero">Volta ${volta.numero}</span>
<span class="tempo">${principal}${ms}</span>
`;
listaVoltas.appendChild(div);
});
}
// ── Eventos ──────────────────────────────────────────
btnStart.addEventListener("click", () => {
rodando ? pausar() : iniciar();
});
btnLap.addEventListener("click", registrarVolta);
btnReset.addEventListener("click", reset);
// Atalhos de teclado
document.addEventListener("keydown", (e) => {
if (e.code === "Space") {
e.preventDefault();
rodando ? pausar() : iniciar();
}
if (e.code === "KeyL" && rodando) registrarVolta();
if (e.code === "KeyR" && !rodando && totalAcumulado > 0) reset();
});
</script>
</body>
</html>
Debounce — controlar a frequência de execução
O debounce é um dos padrões mais importantes com temporizadores. Ele garante que uma função só execute após um período de inatividade — evitando execuções excessivas em eventos como input, scroll e resize:
function debounce(funcao, delay) {
let timer;
return function(...args) {
// Cancela o timer anterior
clearTimeout(timer);
// Agenda nova execução
timer = setTimeout(() => {
funcao.apply(this, args);
}, delay);
};
}
// Uso — busca que só dispara após 500ms de inatividade
const buscar = debounce((termo) => {
console.log(`Buscando por: "${termo}"`);
// Aqui viria uma chamada à API
}, 500);
const input = document.querySelector("#busca");
input.addEventListener("input", (e) => {
buscar(e.target.value);
});
Sem debounce, cada tecla dispararia uma busca. Com debounce, só a última (após a pausa) é executada.
Throttle — limitar execuções por tempo
O throttle garante que uma função execute no máximo uma vez a cada intervalo de tempo — independente de quantas vezes for chamada:
function throttle(funcao, limite) {
let ultima = 0;
return function(...args) {
const agora = Date.now();
if (agora - ultima >= limite) {
ultima = agora;
funcao.apply(this, args);
}
};
}
// Uso — scroll que executa no máximo a cada 200ms
const aoScrollar = throttle(() => {
console.log(`Posição: ${window.scrollY}px`);
}, 200);
window.addEventListener("scroll", aoScrollar);
| Debounce | Throttle | |
|---|---|---|
| Executa | Após período de inatividade | A cada intervalo fixo |
| Uso típico | Busca ao digitar, salvamento automático | Scroll, resize, drag |
| Analogia | Elevador que espera o último passageiro | Semáforo com tempo fixo |
setTimeout recursivo vs setInterval
Uma alternativa ao setInterval que dá mais controle:
// setInterval — intervalo fixo entre inícios de execução
// Se a função demorar 800ms e o intervalo for 1000ms,
// a próxima execução começa 200ms após a anterior terminar
// setTimeout recursivo — intervalo após o término
// Garante que a próxima execução só começa após a anterior terminar
function executarPeriodicamente() {
console.log("Executando...");
// Simula operação demorada
// (com async/await isso faz mais sentido — Módulo 3)
// Agenda próxima execução após terminar
setTimeout(executarPeriodicamente, 1000);
}
// Inicia
setTimeout(executarPeriodicamente, 1000);
Para operações assíncronas (como chamadas à API), o setTimeout recursivo é mais seguro — garante que você não sobreponha execuções.
Boas práticas com temporizadores
// ✅ 1. Sempre guarde o ID para poder cancelar
const id = setTimeout(fn, 1000);
clearTimeout(id);
// ✅ 2. Limpe intervalos quando o componente for destruído
// Isso evita memory leaks e comportamentos fantasma
class Componente {
constructor() {
this.intervalo = setInterval(() => this.atualizar(), 1000);
}
destruir() {
clearInterval(this.intervalo); // essencial!
}
}
// ✅ 3. Use debounce em eventos de alta frequência
window.addEventListener("resize", debounce(() => {
recalcularLayout();
}, 200));
// ✅ 4. Cuidado com this dentro de setInterval
// Arrow functions preservam o this do contexto externo
class Relogio {
constructor() {
this.horas = 0;
// ✅ Arrow function mantém o this correto
setInterval(() => {
this.horas++;
console.log(this.horas);
}, 1000);
}
}
// ✅ 5. setTimeout(fn, 0) para adiar sem bloquear
// Útil para deixar o DOM atualizar antes de executar algo
button.addEventListener("click", () => {
button.textContent = "Carregando...";
setTimeout(() => {
// O DOM já atualizou o texto antes de executar isso
operacaoPesada();
}, 0);
});
Tarefa para você
Construa um timer de Pomodoro com as seguintes funcionalidades:
Regras do Pomodoro:
- 25 minutos de foco
- 5 minutos de pausa curta
- A cada 4 pomodoros, 15 minutos de pausa longa
Funcionalidades:
1. Display de contagem regressiva (MM:SS)
2. Botões: Iniciar / Pausar / Resetar
3. Indicador da fase atual (Foco / Pausa Curta / Pausa Longa)
4. Contador de pomodoros completados
5. Notificação sonora ao mudar de fase
(use: new Audio('...').play() ou o AudioContext)
6. Ao completar um ciclo, avançar para a próxima fase automaticamente
7. Salvar o estado no LocalStorage (para persistir ao recarregar)
Dica para a contagem regressiva:
function iniciarContagem(duracaoMs) {
const fim = Date.now() + duracaoMs;
const intervalo = setInterval(() => {
const restante = fim - Date.now();
if (restante <= 0) {
clearInterval(intervalo);
faseConcluida();
return;
}
atualizarDisplay(restante);
}, 100); // atualiza a cada 100ms para maior precisão
return intervalo;
}
Conclusão
Neste artigo você aprendeu:
setTimeoutpara executar código após um delaysetIntervalpara execuções periódicasclearTimeouteclearIntervalpara cancelar agendamentos- Por que o delay não é exato e como o Event Loop influencia
- O padrão debounce para controlar eventos de alta frequência
- O padrão throttle para limitar execuções por tempo
setTimeoutrecursivo como alternativa segura aosetInterval- Boas práticas para evitar memory leaks e bugs com
this - Como construir um cronômetro completo com voltas e atalhos
📚 Fontes e Referências
- MDN Web Docs — setTimeout: https://developer.mozilla.org/pt-BR/docs/Web/API/setTimeout
- MDN Web Docs — setInterval: https://developer.mozilla.org/pt-BR/docs/Web/API/setInterval
- MDN Web Docs — clearTimeout: https://developer.mozilla.org/pt-BR/docs/Web/API/clearTimeout
- JavaScript.info — Scheduling: setTimeout and setInterval: https://javascript.info/settimeout-setinterval
- JavaScript.info — Debouncing: https://javascript.info/task/debounce
- CSS-Tricks — Debouncing and Throttling Explained: https://css-tricks.com/debouncing-throttling-explained-examples
- Eloquent JavaScript, Cap. 11 — Asynchronous Programming: https://eloquentjavascript.net/11_async.html
- You Don't Know JS: Async & Performance — Kyle Simpson: https://github.com/getify/You-Dont-Know-JS