Call stack, task queue e microtask queue

1. Fundamentos do Call Stack no JavaScript

O Call Stack (pilha de chamadas) é a estrutura fundamental que gerencia a execução do código JavaScript. Funciona como uma pilha LIFO (Last In, First Out), onde cada chamada de função adiciona um novo frame no topo e, quando a função retorna, o frame é removido.

function primeira() {
  console.log('Executando primeira');
  segunda();
  console.log('Finalizando primeira');
}

function segunda() {
  console.log('Executando segunda');
  terceira();
  console.log('Finalizando segunda');
}

function terceira() {
  console.log('Executando terceira');
}

primeira();
// Saída:
// Executando primeira
// Executando segunda
// Executando terceira
// Finalizando segunda
// Finalizando primeira

Quando executamos este código no Node.js, podemos visualizar o Call Stack em ação. Cada função é empilhada sequencialmente e desempilhada na ordem inversa. O Node.js oferece o método console.trace() para inspecionar a pilha em tempo real:

function rastrear() {
  console.trace('Rastreando call stack');
}

function nivel1() {
  nivel2();
}

function nivel2() {
  rastrear();
}

nivel1();
// Trace: Rastreando call stack
//   at rastrear (index.js:2:11)
//   at nivel2 (index.js:7:5)
//   at nivel1 (index.js:3:5)
//   at Object.<anonymous> (index.js:10:1)

2. Call Stack e Bloqueio: O Problema da Execução Síncrona

O maior problema do Call Stack é que ele executa apenas uma tarefa por vez. Operações síncronas pesadas bloqueiam toda a execução, congelando interfaces e impedindo respostas a eventos.

// Código bloqueante no React
function ComponentePesado() {
  const [contador, setContador] = React.useState(0);

  const calcular = () => {
    // Bloqueia o Call Stack por 3 segundos
    const inicio = Date.now();
    while (Date.now() - inicio < 3000) {
      // Loop síncrono pesado
    }
    setContador(c => c + 1);
  };

  return (
    <div>
      <p>Contador: {contador}</p>
      <button onClick={calcular}>Calcular (congela UI)</button>
    </div>
  );
}

Quando o botão é clicado, a interface do React congela completamente. O navegador não consegue processar eventos de clique, animações ou atualizações visuais porque o Call Stack está ocupado com o loop síncrono.

3. Task Queue (Macrotask Queue): A Primeira Camada Assíncrona

A Task Queue (ou Macrotask Queue) armazena tarefas assíncronas como setTimeout, setInterval, operações de I/O e eventos do DOM. O Event Loop verifica se o Call Stack está vazio e, se estiver, move a primeira tarefa da fila para execução.

console.log('Início');

setTimeout(() => {
  console.log('setTimeout 1');
}, 0);

setTimeout(() => {
  console.log('setTimeout 2');
}, 0);

console.log('Fim');

// Saída no Node.js:
// Início
// Fim
// setTimeout 1
// setTimeout 2

Mesmo com setTimeout de 0ms, as callbacks só executam após todo o código síncrono terminar. O Event Loop processa uma macrotask por vez, permitindo que a interface "respire" entre as execuções.

4. Microtask Queue: Prioridade e Promises

A Microtask Queue tem prioridade sobre a Task Queue. Microtasks incluem Promise.then/catch/finally, queueMicrotask() e MutationObserver. Após cada macrotask, o Event Loop processa todas as microtasks disponíveis antes de buscar a próxima macrotask.

console.log('1: Síncrono');

setTimeout(() => {
  console.log('2: Macrotask (setTimeout)');
}, 0);

Promise.resolve()
  .then(() => console.log('3: Microtask (Promise)'))
  .then(() => console.log('4: Microtask (Promise encadeada)'));

queueMicrotask(() => console.log('5: Microtask (queueMicrotask)'));

console.log('6: Síncrono final');

// Saída:
// 1: Síncrono
// 6: Síncrono final
// 3: Microtask (Promise)
// 4: Microtask (Promise encadeada)
// 5: Microtask (queueMicrotask)
// 2: Macrotask (setTimeout)

5. O Ciclo Completo do Event Loop

O algoritmo completo do Event Loop no navegador é:
1. Executa uma macrotask da Task Queue
2. Processa todas as microtasks da Microtask Queue
3. Renderiza a interface (se necessário)
4. Repete o ciclo

No Node.js, o comportamento é similar, mas com adições como process.nextTick:

console.log('1: Início');

setTimeout(() => {
  console.log('2: setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('3: Promise');
});

process.nextTick(() => {
  console.log('4: nextTick');
});

queueMicrotask(() => {
  console.log('5: queueMicrotask');
});

console.log('6: Fim');

// Saída no Node.js:
// 1: Início
// 6: Fim
// 4: nextTick
// 3: Promise
// 5: queueMicrotask
// 2: setTimeout

Note que process.nextTick tem prioridade sobre Promises no Node.js, executando antes de outras microtasks.

6. Implicações no Desenvolvimento com React

No React, o Event Loop afeta diretamente como as atualizações de estado são processadas. O React utiliza batching (agrupamento) de atualizações, que são processadas como microtasks:

function ComponenteExemplo() {
  const [contador, setContador] = React.useState(0);

  React.useEffect(() => {
    console.log('useEffect executou');
  }, [contador]);

  const atualizar = () => {
    setContador(c => c + 1);
    setContador(c => c + 1);
    setContador(c => c + 1);

    // React agrupa as 3 atualizações em uma única renderização
    // O useEffect só executará uma vez
  };

  return <button onClick={atualizar}>Atualizar</button>;
}

O hook useLayoutEffect é síncrono e executa antes da renderização, enquanto useEffect é assíncrono e executa após a renderização. Isso está diretamente relacionado ao Event Loop:

function ComponenteComHooks() {
  React.useLayoutEffect(() => {
    // Executa síncrono, antes da pintura
    console.log('useLayoutEffect - antes da renderização');
  }, []);

  React.useEffect(() => {
    // Executa assíncrono, após a pintura
    console.log('useEffect - após a renderização');
  }, []);

  return <div>Exemplo</div>;
}

7. Boas Práticas e Debugging

Para depurar o Call Stack, utilize as Ferramentas do Desenvolvedor do navegador (aba Sources > Call Stack) ou o Node.js Inspector:

// Node.js: execute com --inspect
function debugar() {
  debugger; // Pausa a execução
  console.log('Depurando');
}

Estratégias para evitar bloqueios:

// Ruim: bloqueia o Call Stack
function processarDadosGrandes(dados) {
  return dados.map(item => {
    // Operação pesada síncrona
    while (Date.now() - inicio < 100) { }
    return item * 2;
  });
}

// Bom: delega para Web Workers (navegador)
const worker = new Worker('worker.js');
worker.postMessage(dados);
worker.onmessage = (event) => {
  console.log('Resultado:', event.data);
};

// Bom: usa Promise.all com microtasks
async function processarLotes(dados) {
  const lotes = dividirEmLotes(dados, 100);
  const resultados = [];

  for (const lote of lotes) {
    // Permite que microtasks e renderização ocorram entre lotes
    await new Promise(resolve => queueMicrotask(resolve));
    resultados.push(processarLote(lote));
  }

  return resultados;
}

Referências