Programação & 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().

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

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

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

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

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

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

JAVASCRIPT
// ✅ 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

  • 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."