Event Loop em JavaScript: O Coração da Concorrência Não Bloqueante — Revisitado em 2025
O Event Loop não é um simples laço, mas um mecanismo sofisticado de coordenação entre Call Stack, queues e runtime. Essencial para concorrência em JS, ele permite milhares de operações assíncronas sem travar a UI. Este guia explora sua estrutura, diferenças entre Node.js/navegador, bugs comuns, técnicas avançadas e tendências para 2025, como Structured Concurrency e scheduler.yield().

O event loop é o mecanismo fundamental que permite JavaScript executar operações assíncronas não bloqueantes. Em 2025, com Node.js 22 e engines mais sofisticadas, entender como o event loop funciona é essencial para desenvolvedores que buscam performance real em aplicações modernas.
Por Que o Event Loop Ainda Importa em 2025?
Se você desenvolve com JavaScript — seja no frontend com React, Vue ou Svelte, ou no backend com Node.js, Bun ou Deno — o event loop está presente em cada operação assíncrona que você executa. Fetch de dados, timers, promessas, manipulação de DOM, processamento de streams: tudo passa por esse mecanismo.
O ponto crítico: a maioria dos desenvolvedores sabe que o event loop existe, mas não entende como ele realmente funciona. E esse conhecimento superficial tem um custo alto em 2025.
Com a popularização de aplicações real-time, WebSockets, Server-Sent Events e arquiteturas serverless, o event loop se tornou o gargalo mais comum em aplicações de alta performance. Entender suas nuances não é mais um "conhecimento bônus" — é uma competência essencial.
O Que É o Event Loop: Fundamentos
O event loop é o mecanismo de concorrência que permite JavaScript executar operações não bloqueantes apesar de ser, por natureza, uma linguagem single-threaded. Essa aparente contradição é resolvida através de um modelo inteligente de filas e ciclos de execução.
console.log('1. Início do script');setTimeout(() => { console.log('2. Timeout executado');}, 0);Promise.resolve().then(() => { console.log('3. Promise microtask');});console.log('4. Fim do script');// Output:// 1. Início do script// 4. Fim do script// 3. Promise microtask// 2. Timeout executadoA Arquitetura de Filas
O event loop opera com múltiplas filas de execução, cada uma com prioridade e comportamento distintos:
// Macrotask queuesetTimeout(() => console.log('Macrotask 1'), 0);setImmediate(() => console.log('Immediate 1')); // Node.js apenas// Microtask queuePromise.resolve().then(() => console.log('Microtask 1'));queueMicrotask(() => console.log('Microtask via queueMicrotask'));// process.nextTick (Node.js - executa antes das microtasks)process.nextTick(() => console.log('nextTick'));// Output em Node.js:// nextTick// Microtask 1// Microtask via queueMicrotask// Macrotask 1// Immediate 1Evolução do Event Loop: De 2015 a 2025
O Cenário em 2015
Em 2015, o event loop era relativamente simples. O V8 estava em sua versão 4.x, e o suporte a Promises nativas era recente (ES6). A maioria dos desenvolvedores ainda usava callbacks, e async/await era uma novidade no horizonte.
O modelo básico era:
Task Queue: onde entravam setTimeout, I/O callbacks, eventos de UI
Microtask Queue: introduzida com Promises, mas pouco utilizada
A Revolução de 2017-2019
Com a padronização de async/await (ES2017) e a introdução de queueMicrotask na especificação, o comportamento das microtasks ficou mais previsível. O V8 otimizou o tratamento de Promises ao longo das versões, reduzindo o overhead de criação de objetos.
async function fetchData(urls) { const results = await Promise.all( urls.map(url => fetch(url).then(r => r.json())) ); return results;}O Estado da Arte em 2025
Em 2025, o ecossistema em torno do event loop amadureceu:
Worker threads maduros: Paralelismo real para tarefas CPU-bound
Async context tracking: Rastreamento de contexto assíncrono via
AsyncLocalStorageBun e Deno: Novos runtimes com implementações otimizadas
scheduler.yield(): API nativa para ceder controle ao event loop (Chrome 124+)
// AsyncLocalStorage — disponível desde Node.js 16, amplamente adotado em 2025import { AsyncLocalStorage } from 'async_hooks';const requestStorage = new AsyncLocalStorage();function handleRequest(req, res) { requestStorage.run({ requestId: generateId() }, () => { logRequest(req); processData(); });}async function processData() { const store = requestStorage.getStore(); console.log(`Processing request: ${store.requestId}`);}Microtasks vs Macrotasks: O Conflito de Prioridades
console.log('=== Início ===');setTimeout(() => console.log('1. setTimeout (macrotask)'), 0);Promise.resolve().then(() => console.log('2. Promise.then (microtask)'));queueMicrotask(() => console.log('3. queueMicrotask (microtask)'));if (typeof process !== 'undefined' && process.nextTick) { process.nextTick(() => console.log('4. nextTick (Node.js only)'));}console.log('=== Fim do script ===');// Output:// === Início ===// === Fim do script ===// 4. nextTick (se Node.js)// 2. Promise.then// 3. queueMicrotask// 1. setTimeoutPor Que Isso Importa?
A ordem de execução afeta diretamente:
Garantias de renderização: Browsers fazem render entre macrotasks
Bloqueio de event loop: Microtasks longas bloqueiam tudo
Ordem de atualizações de estado: React e outros frameworks dependem desse comportamento
// PROBLEMA: loop pesado dentro de microtask bloqueia o event loop inteiroasync function processarDados() { const dados = await buscarDados(); for (let i = 0; i < 10000000; i++) { processarItem(dados[i]); }}// SOLUÇÃO: dividir em chunks cedendo o controle entre cada umasync function processarDadosOtimizado(dados) { for (let i = 0; i < dados.length; i += 1000) { const chunk = dados.slice(i, i + 1000); chunk.forEach(processarItem); await new Promise(resolve => setTimeout(resolve, 0)); }}Benchmarks: Medindo o Event Loop
Os valores abaixo são referências para orientar decisões — execute em seu próprio ambiente para resultados precisos.
import { performance } from 'perf_hooks';function benchmark(name, fn, iterations = 100000) { const start = performance.now(); for (let i = 0; i < iterations; i++) { fn(); } const end = performance.now(); console.log(`${name}: ${((end - start) / iterations * 1000).toFixed(3)}µs por operação`);}benchmark('Promise.then', () => { Promise.resolve().then(() => {});});benchmark('queueMicrotask', () => { queueMicrotask(() => {});});Promise.then / queueMicrotask
setTimeout(0)
Async function overhead
Frações de microssegundo
1–5ms (mínimo imposto pelo runtime)
Similar a Promise.then
Node.js vs Browser: Event Loops Divergentes
Node.js Event Loop (libuv)
┌───────────────────────────┐│ Timers │ setTimeout, setInterval└─────────────┬─────────────┘ ▼┌───────────────────────────┐│ Pending callbacks │ erros de I/O da iteração anterior└─────────────┬─────────────┘ ▼┌───────────────────────────┐│ Poll │ aguarda I/O, executa callbacks└─────────────┬─────────────┘ ▼┌───────────────────────────┐│ Check │ setImmediate└─────────────┬─────────────┘ ▼┌───────────────────────────┐│ Close callbacks │ socket.on('close', ...)└───────────────────────────┘Entre CADA fase, microtasks são executadas.Browser Event Loop
// No browser, o modelo é mais simples:// 1. Execute todo o JavaScript síncrono// 2. Execute todas as microtasks// 3. Render (se necessário)// 4. Execute próxima macrotask// requestAnimationFrame — executa antes do paintrequestAnimationFrame(() => { console.log('Antes do render');});// requestIdleCallback — para trabalho não urgenterequestIdleCallback(() => { console.log('Browser está ocioso');});Anti-Patterns e Boas Práticas em 2025
Erro #1: Confundir setTimeout(0) com setImmediate
// ❌ Assumir que são equivalentessetTimeout(() => console.log('timeout'), 0);setImmediate(() => console.log('immediate'));// A ordem pode variar em Node.js dependendo do contexto de I/O.// setImmediate não existe no browser — use setTimeout(0) lá.Erro #2: Bloquear o event loop com volumes grandes
// ❌ Processa tudo de uma vezasync function importUsers(users) { for (const user of users) { await saveUser(user); }}// ✅ Processa em batches cedendo o controle entre cada umasync function importUsersBatched(users, batchSize = 100) { for (let i = 0; i < users.length; i += batchSize) { const batch = users.slice(i, i + batchSize); await Promise.all(batch.map(saveUser)); await new Promise(resolve => setTimeout(resolve, 0)); reportProgress(Math.min(i + batchSize, users.length), users.length); }}Erro #3: Não usar AbortController para cancelamento
// ✅ Cancelamento limpo com AbortControllerasync function fetchWithCancel(url, signal) { const response = await fetch(url, { signal }); return response.json();}const controller = new AbortController();fetchWithCancel('/api/data', controller.signal) .then(data => console.log(data)) .catch(err => { if (err.name === 'AbortError') { console.log('Request cancelado'); } });setTimeout(() => controller.abort(), 5000);Conclusão
O event loop não é um detalhe de implementação — é o modelo de execução do JavaScript. Ignorar seu funcionamento é escrever código com bugs de timing esperando para aparecer em produção.
✅ Microtasks para atualizações atômicas de estado
✅ Chunks + yield para processamento de volumes grandes
✅
requestIdleCallbackpara tarefas não urgentes no browser✅
AbortControllerpara cancelamento limpo✅ Worker threads para trabalho CPU-bound de verdade
🔗 Referências
MDN — Execution model
Node.js Docs — The Node.js Event Loop
V8 Blog — v8.dev/blog
HTML Spec — Event Loop Processing Model
Takeaway: "Em JavaScript, o tempo não é linear — é uma fila de tarefas, microtarefas e callbacks. Quem entende a fila, entende o tempo."


