O event loop: porque é que um ciclo bloqueia a página inteira
O JavaScript tem uma thread só. É a mesma que corre o seu código, responde aos cliques e desenha a página — e é por isso que um ciclo pesado congela tudo, incluindo o cursor a piscar numa caixa de texto.
1. O modelo, em três peças
A pilha — o que está a correr agora. Uma coisa de cada vez.
As tarefas — o que está à espera de vez: um
setTimeoutque expirou, um clique, uma resposta da rede.As microtarefas — as continuações de promessas. Têm fila própria, e prioridade.
O ciclo é simples: correr até a pilha esvaziar, esvaziar toda a fila de microtarefas, e só então pegar na tarefa seguinte.
2. A ordem que confunde toda a gente
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 1, 4, 3, 2O setTimeout com zero não quer dizer "já": quer dizer "põe na fila das tarefas". A promessa vai para a fila das microtarefas, que é esvaziada primeiro — daí o 3 antes do 2.
3. Onde isto morde
// Meio segundo em que nada responde: nem cliques, nem scroll, nem animações
const resultado = listaDeCemMil.map(calculoPesado);A pilha não esvazia enquanto o map não acabar, portanto o browser não chega a desenhar nada. É exatamente isto que a métrica INP mede.
4. As saídas
Partir o trabalho, para o browser respirar entre pedaços:
async function emPedacos(itens, tarefa, tamanho = 500) {
for (let i = 0; i < itens.length; i += tamanho) {
itens.slice(i, i + tamanho).forEach(tarefa);
// Devolve a vez ao browser: ele desenha, responde, e continua
await new Promise((resolve) => setTimeout(resolve, 0));
}
}Ou tirá-lo da thread principal de vez, com um worker:
const trabalhador = new Worker('./calculo.js');
trabalhador.postMessage(listaDeCemMil);
trabalhador.addEventListener('message', (e) => mostrar(e.data));E a nota que fecha o assunto: async não é paralelismo. Um await liberta a thread enquanto espera por algo — rede, disco, um temporizador. Não a liberta enquanto calcula.