Trilha 11 · Como um navegador funciona

O event loop do JavaScript

JavaScript é single-thread — mas executa código assíncrono. O event loop é o mecanismo que orquestra tudo isso, e entendê-lo explica comportamentos que parecem mágicos.

① Intuição

Um único thread, muitas tarefas

JavaScript é single-threaded — executa uma coisa de cada vez. Mas você pode fazer requests, ouvir cliques e usar timers sem travar. Como? O browser fornece Web APIs (setTimeout, fetch, DOM events) que são executadas fora do thread JS e, quando prontas, adicionam callbacks a filas.

O event loop monitora essas filas: quando a Call Stack esvazia, ele pega a próxima tarefa. Mas há duas filas com prioridades diferentes: Microtask Queue (Promises, await) tem prioridade e é esvaziada completamente antes de qualquer Task Queue (setTimeout, eventos DOM).

Por que isso importa? Se você usar muitas Promises encadeadas, o browser pode não ter chance de renderizar entre elas — a tela trava. Se precisar liberar o thread para renderização, use setTimeout(fn, 0) ou scheduler.postTask() para criar uma macrotask.
② Visualização interativa

Veja o event loop em ação

Escolha um snippet, clique em Próximo passo para avançar ou Executar para animar automaticamente. Observe como itens se movem entre Call Stack, Web APIs e filas.

console.log("A");
setTimeout(() => {
  console.log("C");
}, 0);
console.log("B");
CALL STACK
main
WEB APIS
(nenhum)
MICROTASK QUEUE
(vazia)
TASK QUEUE
(vazia)
CONSOLE OUTPUT
(sem saída ainda)
PASSO 1 / 9
Script inicia. "main" entra na Call Stack.
③ Explicação técnica

O loop em pseudocódigo

// O Event Loop em pseudocódigo

while (true) {
  // 1. Executar a tarefa atual (macrotask)
  executar(taskQueue.shift());

  // 2. Esvaziar TODA a microtask queue
  while (microtaskQueue.length) {
    executar(microtaskQueue.shift());
  }

  // 3. Se necessário, renderizar (a cada ~16ms)
  if (precisaRenderizar()) renderizar();
}

// Fontes de macrotasks (Task Queue):
setTimeout(fn, 0)     // mínimo ~4ms no browser
setInterval(fn, 100)
fetch().then()        // NÃO — fetch callback é microtask!
eventos DOM (click, keydown…)

// Fontes de microtasks (Microtask Queue):
Promise.resolve().then(fn)
queueMicrotask(fn)
await (dentro de async function)

Evolução do código assíncrono

// Três formas de código assíncrono

// 1. Callbacks (antigo) — "callback hell"
setTimeout(() => {
  fetch(url, (dados) => {
    processar(dados, (resultado) => {
      salvar(resultado);  // pirâmide da desgraça
    });
  });
}, 1000);

// 2. Promises — encadeável, tratamento de erro claro
fetch(url)
  .then(r => r.json())
  .then(dados => processar(dados))
  .catch(e => console.error(e));

// 3. async/await — síncrono de ler, assíncrono de executar
async function buscar() {
  try {
    const dados = await fetch(url).then(r => r.json());
    return processar(dados);
  } catch (e) {
    console.error(e);
  }
}
await não bloqueia o thread. await promise suspende a função atual e devolve o controle ao event loop. O thread continua livre para executar outros callbacks. Quando a promise resolve, o event loop agenda a continuação como uma microtask. Isso explica por que código após await não executa "imediatamente" — ele vai para a microtask queue primeiro.
④ Projeto para programar

Implemente um scheduler simples

Mini projeto: implemente uma fila de tarefas com prioridade usando queueMicrotask e setTimeout. Execute 5 tarefas de alta prioridade (microtask) intercaladas com 5 de baixa (macrotask). Observe e documente a ordem de execução.

Projeto principal: construa um debounce e um throttle do zero. Debounce aguarda que o usuário pare de digitar antes de chamar a função. Throttle limita chamadas a uma por intervalo. Use setTimeout/clearTimeout.

Desafio extra: implemente Promise.all, Promise.race e Promise.allSettled do zero usando o construtor new Promise((resolve, reject) => {}). Use seus implementações para buscar 3 APIs em paralelo.

⑤ Exercícios rápidos

Teste sua intuição

console.log("início"); setTimeout(()=>console.log("T"),0); Promise.resolve().then(()=>console.log("P")); console.log("fim"); — qual é a saída?
Qual das opções abaixo tem a maior prioridade no event loop?
O que acontece quando o código encontra um await?
⑥ Aplicações no mundo real

Onde você encontra isso

🚀

Node.js

Node.js usa o mesmo event loop (via libuv). Operações de I/O (arquivo, rede) nunca bloqueiam o thread — elas delegam para o sistema operacional e recebem callbacks. É por isso que Node serve milhares de conexões simultâneas com um único thread.

⚛️

React Concurrent Mode

O React 18 usa scheduler.postTask() para dividir o trabalho de render em pedaços menores, cedendo ao browser para renderizar entre eles. Isso elimina o "jank" em árvores de componentes grandes.

🧵

Web Workers

Para trabalho CPU-intensivo (parsear CSV grande, processar imagens), use Web Workers — threads separados que não bloqueiam o thread principal. Comunicam via postMessage.

📡

WebSockets e SSE

Conexões em tempo real (chat, preço ao vivo) usam callbacks de eventos DOM (ws.onmessage). Cada mensagem entra na Task Queue e processa no próximo ciclo do event loop — sem polling.

← Anterior: Render Pipeline Próxima: Segurança →