O event loop do Node.js é o loop que decide qual callback roda em seguida depois que o seu código síncrono termina. Ele passa por fases em um ciclo fixo: timers (setTimeout, setInterval), pending callbacks, poll (I/O, como leitura de arquivos e dados de rede), check (setImmediate) e close callbacks. Depois de cada callback, o Node esvazia primeiro a fila do process.nextTick e depois a fila de promises, então elas sempre rodam antes de o loop seguir em frente.
Ele imprime sync, nextTick, promise, setTimeout, o contrário da ordem em que foram escritos. O código síncrono sempre termina primeiro, depois o Node esvazia a fila do nextTick, depois a fila de promises, e só então o loop começa e chega na fase de timers.
As fases do event loop
O loop do Node vem da libuv, a biblioteca em C que o Node usa para I/O. Antes da primeira volta ele executa os timers que já venceram, e depois cada volta executa estas fases em ordem:
- Pending callbacks: alguns callbacks do sistema adiados da volta anterior, como certos erros de rede.
- Poll: novos eventos de I/O e seus callbacks (uma leitura de arquivo terminou, chegaram dados em um socket). Quando não há mais nada agendado, o loop espera aqui.
- Check: callbacks de
setImmediate. - Close callbacks: eventos
'close', como um socket que foi destruído. - Timers: callbacks de
setTimeoutesetIntervalcujo tempo chegou.
Muitos guias colocam os timers primeiro em toda volta, que é como o Node funcionava antes da versão 20 (libuv 1.45). Os callbacks rodam na mesma ordem de qualquer jeito: os timers de uma volta são seguidos pelo poll da volta seguinte.
Duas filas ficam fora das fases: a fila do nextTick e a fila de microtasks (callbacks de promises e queueMicrotask). O Node esvazia as duas depois de cada callback, em qualquer fase, o nextTick primeiro. O loop continua girando enquanto houver algo para esperar (um timer, um arquivo ou socket aberto, um servidor), e o processo termina quando não sobra nada.
Este é o loop do próprio Node. O navegador compartilha a ideia de tasks e microtasks, mas não tem fases, nem setImmediate, nem process.nextTick; a página do event loop do JavaScript mostra a visão no nível da linguagem.
process.nextTick vs promises
Os dois rodam antes de o loop continuar, mas não se misturam. O Node roda a fila inteira do nextTick, incluindo ticks adicionados enquanto ela roda, e só então a fila de promises. Tente adivinhar este:
A ordem é tick 1, tick 2, tick from tick, promise 1, promise 2. O tick agendado dentro de outro tick entra na mesma fila e ainda roda antes de qualquer promise, mesmo a promise tendo sido criada primeiro.
No nível superior de um ES module (um arquivo que usa import) os dois trocam de lugar: o próprio código do módulo roda dentro de um job de promise, então os callbacks de promise dele terminam antes de o Node chegar à fila do nextTick. Dentro de callbacks, a regra do nextTick primeiro vale nos dois sistemas de módulos.
setImmediate vs setTimeout(fn, 0)
setTimeout(fn, 0) na verdade significa "depois de pelo menos 1 ms, na fase de timers". setImmediate(fn) significa "na próxima fase de check". Qual roda primeiro depende de onde você os chama.
Dentro de um callback de I/O a ordem é fixa. O callback roda na fase de poll, a fase de check vem logo depois, e a fase de timers só vem depois disso:
Ele imprime nextTick, setImmediate, setTimeout, sempre. Isso faz do setImmediate a escolha certa para "rode isto depois do trabalho de I/O atual".
No módulo principal, fora de qualquer callback, a ordem dos dois não é fixa:
// Order depends on how fast the process started
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
Se pelo menos 1 ms já passou quando o loop começa, o timer está vencido e imprime primeiro; senão o setImmediate ganha. Não escreva código que dependa de nenhum dos dois resultados.
| Roda | Use para | |
|---|---|---|
process.nextTick(fn) | Logo depois da operação atual, antes das promises | Emitir um evento ou erro depois que quem chamou registrou os listeners |
Promise.then, queueMicrotask | Depois que a fila do nextTick esvazia | Código assíncrono comum |
setImmediate(fn) | Fase de check, depois dos callbacks de I/O | Devolver a vez ao loop entre pedaços de trabalho |
setTimeout(fn, 0) | Fase de timers, pelo menos 1 ms depois | Atrasos em que o momento exato não importa |
Bloquear o event loop
Como existe uma única thread de JavaScript, um callback que roda por muito tempo para todo o resto: timers, callbacks de I/O e, em um servidor, todas as outras requisições. O atraso de um timer é um mínimo, não uma garantia.
loop finished imprime primeiro, depois true: o timer vencia em 10 ms, mas não pôde rodar até o loop ocupado devolver a thread. Em um servidor, o mesmo loop dentro de um handler de rota congela todas as outras requisições por meio segundo.
A solução para trabalho longo é dividi-lo e dar uma vez ao loop entre os pedaços com setImmediate:
O timer entra depois da parte 1. Troque setImmediate(() => work(part + 1)) por work(part + 1) e rode de novo: as três partes rodam de uma vez e timer imprime por último.
Para trabalho pesado do começo ao fim, como processamento de imagens, dividir não basta: tire esse trabalho da thread principal com worker_threads.
Erros comuns
Chamar process.nextTick recursivamente. Um callback de nextTick que fica agendando outro nextTick nunca deixa o loop continuar, então timers e I/O passam fome. Use setImmediate para trabalho repetido.
Usar APIs síncronas em um servidor. fs.readFileSync, crypto.pbkdf2Sync e chamadas grandes de JSON.parse bloqueiam o loop para todos os usuários. Use as versões assíncronas dentro dos handlers de requisição.
Esperar que setTimeout(fn, 0) rode na hora. Ele roda depois de todo o código síncrono, de todo nextTick e de todo callback de promise, e só quando o loop chega à fase de timers.
Perguntas frequentes
O Node.js é single-threaded?
Seu JavaScript roda em uma thread só, então dois callbacks nunca rodam ao mesmo tempo. O próprio Node usa mais threads: a libuv mantém um pool (quatro threads por padrão) para trabalho de sistema de arquivos, consultas DNS e parte da criptografia, e devolve os resultados para o loop.
Quando um programa Node.js termina?
Quando o event loop não tem mais nada para esperar: nenhum timer, nenhum I/O pendente, nenhum servidor ou socket aberto. Um setInterval rodando ou um servidor escutando mantém o processo vivo até você limpar ou fechar.
O event loop do Node.js é igual ao do navegador?
A ideia é a mesma, uma thread executando callbacks enfileirados, mas os detalhes mudam. O navegador não tem setImmediate nem process.nextTick e encaixa a renderização entre as tarefas, enquanto o Node roda as fases da libuv. A página do event loop do JavaScript cobre a parte comum, da linguagem.
Como saber se o event loop está bloqueado?
Meça o atraso: monitorEventLoopDelay() do perf_hooks registra a demora, e uma média na casa das dezenas de milissegundos significa que algum callback segura a thread por tempo demais. Rode o Node com --cpu-prof para descobrir qual função é.