Este contenido solo está disponible en Portugués.
Aún sin traducción para este idioma.
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 queue
setTimeout(() => console.log('Macrotask 1'), 0);
setImmediate(() => console.log('Immediate 1')); // Node.js apenas
// Microtask queue
Promise.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
AsyncLocalStorage - Bun 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 2025
import { 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 inteiro
async 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 um
async 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(() => {});
});Operação
Promise.then / queueMicrotask
setTimeout(0)
Async function overhead
Ordem de grandeza
Frações de microssegundo
1–5ms (mínimo imposto pelo runtime)
Similar a Promise.then
A diferença prática: setTimeout(0) é ordens de magnitude mais lento que microtasks para scheduling interno. Nunca use setTimeout(0) para coordenar atualizações de estado.
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 paint
requestAnimationFrame(() => {
console.log('Antes do render');
});
// requestIdleCallback — para trabalho não urgente
requestIdleCallback(() => {
console.log('Browser está ocioso');
});Anti-Patterns e Boas Práticas em 2025
Erro #1: Confundir setTimeout(0) com setImmediate
// ❌ Assumir que são equivalentes
setTimeout(() => 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 vez
async function importUsers(users) {
for (const user of users) {
await saveUser(user);
}
}
// ✅ Processa em batches cedendo o controle entre cada um
async 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 AbortController
async 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."



