Este contenido solo está disponible en Portugués.

Aún sin traducción para este idioma.

Programacion & Dev

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().

Equipe Blueprintblog8 min
Event Loop em JavaScript: O Coração da Concorrência Não Bloqueante — Revisitado em 2025

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.

javascript
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 executado

A Arquitetura de Filas

O event loop opera com múltiplas filas de execução, cada uma com prioridade e comportamento distintos:

javascript
// 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 1

Evoluçã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.

javascript
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:

  1. Worker threads maduros: Paralelismo real para tarefas CPU-bound
  2. Async context tracking: Rastreamento de contexto assíncrono via AsyncLocalStorage
  3. Bun e Deno: Novos runtimes com implementações otimizadas
  4. scheduler.yield(): API nativa para ceder controle ao event loop (Chrome 124+)
javascript
// 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

javascript
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. setTimeout

Por 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
javascript
// 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.

javascript
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)

text
┌───────────────────────────┐
│  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

javascript
// 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

javascript
// ❌ 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

javascript
// ❌ 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

javascript
// ✅ 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
  • requestIdleCallback para tarefas não urgentes no browser
  • AbortController para cancelamento limpo
  • ✅ Worker threads para trabalho CPU-bound de verdade

🔗 Referências

  1. MDN — Execution model
  2. Node.js Docs — The Node.js Event Loop
  3. V8 Blog — v8.dev/blog
  4. 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."

Etiquetas del articulo

Articulos relacionados

Recibe los ultimos articulos en tu correo.

Follow Us: