Menu

Event loop do Node.js: fases, nextTick e setImmediate

O event loop do Node.js executa seus callbacks em fases (timers, poll, check, close), e entre um callback e outro esvazia a fila do process.nextTick e depois a fila de promises. Rode os exemplos para ver a ordem exata.

Esta página tem editores executáveis. Edite, execute e veja a saída na hora.

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:

  1. Pending callbacks: alguns callbacks do sistema adiados da volta anterior, como certos erros de rede.
  2. 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.
  3. Check: callbacks de setImmediate.
  4. Close callbacks: eventos 'close', como um socket que foi destruído.
  5. Timers: callbacks de setTimeout e setInterval cujo 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.

RodaUse para
process.nextTick(fn)Logo depois da operação atual, antes das promisesEmitir um evento ou erro depois que quem chamou registrou os listeners
Promise.then, queueMicrotaskDepois que a fila do nextTick esvaziaCódigo assíncrono comum
setImmediate(fn)Fase de check, depois dos callbacks de I/ODevolver a vez ao loop entre pedaços de trabalho
setTimeout(fn, 0)Fase de timers, pelo menos 1 ms depoisAtrasos 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 é.

Ilustração das linguagens de programação do Coddy

Aprenda a programar com o Coddy

Começar