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
- MDN Web Docs: Event Loop — Documentação oficial sobre o Event Loop, Call Stack e filas assíncronas no JavaScript
- Node.js Documentation: The Node.js Event Loop — Guia oficial do Node.js sobre o Event Loop, timers e process.nextTick
- JavaScript Visualized: Event Loop — Ferramenta interativa para visualizar o Event Loop, Call Stack e filas de tarefas
- React Documentation: useEffect vs useLayoutEffect — Documentação oficial do React sobre a diferença entre hooks e seu relacionamento com o Event Loop
- What the heck is the event loop anyway? — Palestra de Philip Roberts que explica visualmente o Event Loop do JavaScript
- JavaScript.info: Microtasks and Macrotasks — Tutorial detalhado sobre microtasks, macrotasks e o ciclo completo do Event Loop