Event Loop do JavaScript, Microtarefas e Macrotarefas
Aprenda como o event loop do JavaScript funciona e como microtarefas (Promises) e macrotarefas (timers, eventos) são agendadas, com exemplos executáveis.
O JavaScript executa seu código em uma única thread: uma coisa acontece por vez, de cima para baixo. Mesmo assim, ele consegue buscar dados, executar timers e responder a cliques sem travar. O mecanismo que torna isso possível é o event loop, e o trabalho que ele agenda é dividido em dois tipos de tarefas: microtarefas e macrotarefas. Esta página explica o que é cada uma, a ordem em que são executadas e as armadilhas que costumam pegar as pessoas de surpresa — todos os exemplos aqui são executáveis para que você possa verificar a saída por conta própria.
Como Funciona o Event Loop
O event loop é o agendador que decide qual trecho de código será executado a seguir. Para entendê-lo, você precisa de apenas três partes:
- Call stack — onde seu código realmente é executado. As funções são empilhadas quando chamadas e removidas quando retornam. O JavaScript executa tudo que está na pilha até o fim antes de fazer qualquer outra coisa; esta é a regra de run-to-completion.
- Heap — a memória onde seus objects vivem. Não está diretamente envolvida no agendamento, mas é a terceira peça que as pessoas esperam ver mencionada.
- Filas de tarefas — trabalho pendente aguardando que a pilha esteja vazia. Existem duas: a fila de macrotarefas (timers, eventos de UI, I/O) e a fila de microtarefas (callbacks de Promise e
queueMicrotask).
Um ciclo do event loop funciona assim:
- Executa a tarefa atual na pilha até que a pilha esteja completamente vazia.
- Esvazia a fila de microtarefas inteira — incluindo quaisquer microtarefas adicionadas durante o esvaziamento.
- (Em um navegador) renderiza quaisquer atualizações visuais pendentes.
- Retira uma macrotarefa da fila de macrotarefas e a executa, depois volta ao passo 2.
A assimetria principal: após cada macrotarefa, o motor esvazia todas as microtarefas, mas só retira uma macrotarefa por ciclo do loop. Essa única regra explica quase toda surpresa de ordenação que você vai encontrar.
Aqui está a demonstração mais simples possível, usando setTimeout para agendar uma macrotarefa:
Neste exemplo:
console.log('Start');é executado primeiro, imprimindo "Start" no console.setTimeoutagenda um callback para ser executado após pelo menos 1000 milissegundos. Ele retorna instantaneamente e não bloqueia as linhas abaixo dele.console.log('End');é executado imediatamente, imprimindo "End".- Somente depois que o script síncrono termina (e o atraso decorreu) o event loop retira o callback do
setTimeoutda fila de macrotarefas e o executa, imprimindo "Timeout Callback".
A saída é Start, End e depois Timeout Callback — o callback do timer aguarda mesmo tendo sido escrito no meio. O callback do setTimeout é uma macrotarefa: ele só é executado depois que o script em execução atual e todas as microtarefas pendentes forem concluídas. É isso que mantém a página responsiva — o código síncrono nunca precisa esperar por um timer ou requisição de rede.
Microtarefas vs. Macrotarefas
O que são Macrotarefas?
Uma macrotarefa (também chamada simplesmente de "task") é uma unidade de trabalho única e autossuficiente que o motor processa uma vez por ciclo do loop. As fontes mais comuns são:
setTimeout/setInterval: timers que executam um callback após um atraso ou repetidamente.- Eventos DOM: um handler de
click,scrollouinput. - I/O: respostas de rede, leituras de arquivo e similares.
O motor executa exatamente uma macrotarefa, depois esvazia todas as microtarefas, depois (no navegador) pode renderizar, antes de pegar a próxima macrotarefa. Portanto, as macrotarefas nunca são executadas de forma consecutiva sem que a fila de microtarefas seja esvaziada entre elas.
O que são Microtarefas?
Uma microtarefa é uma tarefa curta que o motor deseja concluir assim que a unidade de código atual terminar — antes de ceder para a próxima macrotarefa ou para a renderização. Elas vêm de:
- Callbacks de Promise: as funções passadas para
.then(),.catch()e.finally(), além do corpo de uma funçãoasyncapós umawait. queueMicrotask(fn): uma função nativa que agenda uma função diretamente na fila de microtarefas.
A diferença crucial: após a tarefa atual, o motor esvazia a fila de microtarefas inteira antes de fazer qualquer outra coisa. Se uma microtarefa agendar outra microtarefa, a nova também é executada no mesmo esvaziamento — antes que a próxima macrotarefa tenha vez.
Exemplos de Código do Mundo Real
Exemplo 1: Um timer é uma macrotarefa
Imagine que você quer exibir uma mensagem após 2 segundos. A linha de agendamento é executada agora; o callback fica na fila de macrotarefas até que o atraso passe e a pilha esteja livre.
Explicação: setTimeout retorna instantaneamente, então ambas as linhas console.log fora dele são executadas primeiro. O callback é uma macrotarefa que só é executada depois que o script síncrono terminar e o timer disparar. Em um navegador, você normalmente atualizaria o DOM dentro do callback, por exemplo document.getElementById('message').textContent = 'Hello there!';.
Exemplo 2: Um callback de Promise é uma microtarefa
O callback .then() de uma Promise resolvida não é executado de forma inline — ele é enfileirado como uma microtarefa e é executado assim que o código síncrono atual termina.
Explicação: A saída é Before the promise, After the promise e depois Promise resolved (microtask). Mesmo que a Promise já esteja resolvida, o seu callback .then() aguarda na fila de microtarefas até que o código síncrono termine — e então é executado antes de qualquer timer.
Mais sobre a Prioridade de Micro e Macrotarefas
As microtarefas sempre têm prioridade maior do que as macrotarefas. Após o script atual terminar, o motor esvazia todas as microtarefas pendentes antes de processar uma única macrotarefa — mesmo um setTimeout(..., 0) que foi agendado primeiro. Observe no exemplo abaixo que a Promise 2 encadeada, criada dentro de uma microtarefa, ainda é executada antes de qualquer timer, porque a fila de microtarefas é esvaziada completamente antes de o loop continuar.
Saída esperada:
Start
End
Promise 1
Promise 2
Timeout 1
Timeout 2Isso mostra que as microtarefas são executadas imediatamente após o código síncrono, mesmo antes dos timers agendados para o mesmo momento. A priorização significa que as atualizações baseadas em Promise são resolvidas o mais rápido possível.
Uma Armadilha: Starvation de Microtarefas
Como o motor esvazia a fila de microtarefas inteira antes da próxima macrotarefa ou de uma renderização, uma microtarefa que continua agendando mais microtarefas pode bloquear tudo — os timers nunca disparam e a página não consegue repintar. Isso é chamado de starvation de microtarefas:
As cinco microtarefas são todas executadas antes do callback do setTimeout, mesmo que o timer tenha sido agendado primeiro. Em um aplicativo real, uma versão ilimitada desse loop congelaria a interface. A solução é dividir o trabalho de longa duração em macrotarefas (por exemplo, setTimeout(..., 0)), o que permite que o event loop renderize e processe eventos entre os trechos.
Quando Usar Cada Um
- Use microtarefas (Promises,
queueMicrotask) quando quiser que o código seja executado assim que a operação atual terminar, mas ainda de forma assíncrona — como reagir a dados logo após umfetchser resolvido. - Use macrotarefas (
setTimeout, divisão de trabalho entre timers) quando quiser deliberadamente ceder ao navegador para que ele possa renderizar ou processar entradas antes de continuar — por exemplo, dividindo uma computação pesada em partes para que a página permaneça responsiva.
Conclusão
O event loop executa seu código síncrono até o fim, depois esvazia todas as microtarefas, depois processa uma macrotarefa, e repete. Microtarefas (callbacks de Promise, queueMicrotask) sempre são executadas antes da próxima macrotarefa (timers, eventos, I/O). Internalizar essa única regra permite prever a ordem exata de qualquer código assíncrono.
Para se aprofundar, continue com Promises, encadeamento de Promises, async/await e o capítulo dedicado a microtarefas. Para as APIs de timer usadas aqui, consulte agendamento com setTimeout e setInterval.