O que é Event Loop e sua Estrutura Fundamental
O Event Loop é o mecanismo central que permite ao JavaScript executar código assíncrono em uma linguagem single-threaded. Compreender sua estrutura é essencial para escrever código performático e previsível. O Event Loop não é parte da especificação do JavaScript em si, mas sim da implementação das engines (como V8 do Chrome), trabalhando em conjunto com as APIs fornecidas pelo ambiente (browser ou Node.js).
A execução segue um ciclo: o Event Loop verifica a Call Stack, executa o código síncrono, depois processa as filas de microtasks e macrotasks. Esse processo se repete continuamente. A ordem de execução é: Call Stack → Microtasks → Rendering → Macrotasks → (volta ao início). Compreender essa sequência é crucial para prever quando seu código será executado.
Microtasks vs Macrotasks: A Hierarquia de Prioridades
O que são Microtasks?
Microtasks têm prioridade máxima e são executadas após cada macrotask. Elas incluem: Promise.then(), Promise.catch(), Promise.finally(), MutationObserver e queueMicrotask(). A característica distintiva é que todas as microtasks na fila são processadas antes de qualquer macrotask.
console.log('1. Sincronizado');
setTimeout(() => {
console.log('2. Macrotask (setTimeout)');
}, 0);
Promise.resolve()
.then(() => {
console.log('3. Microtask (Promise)');
})
.then(() => {
console.log('4. Microtask (Promise 2)');
});
console.log('5. Sincronizado 2');
// Saída:
// 1. Sincronizado
// 5. Sincronizado 2
// 3. Microtask (Promise)
// 4. Microtask (Promise 2)
// 2. Macrotask (setTimeout)
O que são Macrotasks?
Macrotasks incluem: setTimeout(), setInterval(), setImmediate() (Node.js), eventos DOM (click, load), e requisições HTTP. Apenas uma macrotask é executada por ciclo do Event Loop. Após cada macrotask, todas as microtasks pendentes são processadas antes de qualquer macrotask subsequente.
setTimeout(() => {
console.log('Macrotask 1');
Promise.resolve().then(() => console.log('Microtask após Macrotask 1'));
}, 0);
setTimeout(() => {
console.log('Macrotask 2');
}, 0);
// Saída:
// Macrotask 1
// Microtask após Macrotask 1
// Macrotask 2
Essa hierarquia existe porque Promises são fundamentais para o modelo assíncrono moderno, enquanto macrotasks são operações mais pesadas que exigem controle de quando são executadas.
requestAnimationFrame: O Timing Perfeito para Animações
Onde se Encaixa no Event Loop?
requestAnimationFrame() (rAF) não é exatamente um microtask nem um macrotask. É executado entre as microtasks e a renderização, garantindo que seu código rode sincronizado com a taxa de refresh da tela (60fps ou 120fps). Isso o torna ideal para animações e manipulação do DOM.
console.log('1. Início');
setTimeout(() => {
console.log('2. setTimeout');
}, 0);
requestAnimationFrame(() => {
console.log('3. requestAnimationFrame');
});
Promise.resolve().then(() => {
console.log('4. Promise');
});
console.log('5. Fim síncrono');
// Saída:
// 1. Início
// 5. Fim síncrono
// 4. Promise
// 3. requestAnimationFrame (antes da renderização)
// 2. setTimeout
Exemplo Prático: Animação Eficiente
let position = 0;
function animate() {
position += 5;
const element = document.getElementById('box');
element.style.left = position + 'px';
if (position < 300) {
requestAnimationFrame(animate);
}
}
// Usar rAF é mais eficiente que setInterval
// porque sincroniza com a renderização do navegador
requestAnimationFrame(animate);
// Evite isto:
// setInterval(() => {
// position += 5;
// element.style.left = position + 'px';
// }, 16); // Pode desincronizar do refresh
Casos Práticos: Integrando Tudo Junto
Caso 1: Carregamento de Dados com Animação
async function fetchAndAnimate() {
console.log('1. Iniciando fetch');
// Macrotask: requisição HTTP
const response = await fetch('/api/data');
// Microtask: processamento do Promise
const data = await response.json();
console.log('2. Dados recebidos');
// rAF: renderização suave
requestAnimationFrame(() => {
document.getElementById('content').innerHTML = data.message;
console.log('3. DOM atualizado');
});
}
// setTimeout garante execução assíncrona
setTimeout(() => {
fetchAndAnimate();
}, 0);
Caso 2: Batching de Atualizações
const updates = [];
function scheduleUpdate(callback) {
updates.push(callback);
// Microtask: agrupa múltiplas atualizações
Promise.resolve().then(() => {
console.log(`Processando ${updates.length} atualizações`);
updates.forEach(cb => cb());
updates.length = 0;
});
}
// Chamadas síncronas
scheduleUpdate(() => console.log('Update 1'));
scheduleUpdate(() => console.log('Update 2'));
scheduleUpdate(() => console.log('Update 3'));
// Saída:
// Processando 3 atualizações
// Update 1
// Update 2
// Update 3
Esse padrão é usado internamente por frameworks como React para otimizar renderizações.
Conclusão
Os três conceitos principais que você deve dominar são: (1) Microtasks sempre executam antes de macrotasks, garantindo que Promises sejam processadas com alta prioridade; (2) requestAnimationFrame sincroniza seu código com a renderização do navegador, tornando-o essencial para animações e atualizações visuais eficientes; (3) Compreender essa ordem de execução permite otimizar performance, evitar race conditions e escrever código previsível. Na prática, use Promises para lógica assíncrona crítica, setTimeout para atrasar execução, e rAF para qualquer coisa visual.