Concorrência em TypeScript: do Event Loop aos Web Workers
\n\nVocê já notou como seu navegador ou aplicação Node.js "trava" durante a execução de uma operação matemática complexa ou ao processar um grande JSON? Aquela sensação familiar de impotência, quando a interface para de responder a cliques e o servidor para de responder a novas requisições. Tudo isso porque, por padrão, JavaScript (e seu irmão mais rigoroso TypeScript) é uma linguagem de thread única. Mas isso não significa que estamos condenados a programas lentos. Pelo contrário, o uso adequado da concorrência em TypeScript permite criar aplicações rápidas e responsivas.
\n\nNeste artigo, não vamos apenas listar termos. Vamos entender como realmente funcionam async/await, o que está por baixo do capô do Event Loop, se é possível usar "goroutines" em TypeScript como em Go, e quando devemos usar threads reais através de Web Workers. Vamos lá!
\n\n1. Assincronia vs Paralelismo: qual a diferença?
\n\nAntes de nos aprofundarmos no código, vamos definir os termos. Muitos confundem esses conceitos, e isso é um erro.
\n\n- \n
- Assincronia (concorrência) — é a capacidade do programa de iniciar uma tarefa, mudar para outra enquanto a primeira espera (por exemplo, uma resposta do servidor) e retornar a ela mais tarde. Em um dado momento, apenas uma instrução é executada. \n
- Paralelismo — é a execução de duas ou mais tarefas literalmente ao mesmo tempo, em diferentes núcleos da CPU. \n
TypeScript nos oferece assincronia nativamente. Já o paralelismo exige a criação de threads separadas — no navegador, são os Web Workers; no Node.js, são os worker_threads. Lembre-se dessa diferença, caso contrário, você corre o risco de escrever um código que "trava" devido ao bloqueio da thread principal.
\n\n2. Event Loop e callbacks — a base da concorrência em TypeScript
\n\nO grande segredo da assincronia em JavaScript/TypeScript é o Event Loop (ciclo de eventos). Imagine que você tem uma fila de tarefas. A thread principal pega uma tarefa, a executa; se for síncrona, o resultado é imediato. Se for uma operação assíncrona (como setTimeout ou fetch), ela é enviada "para o lado" (para a Web API ou libuv), enquanto a thread principal continua executando o próximo código.
Quando a operação assíncrona termina, seu callback entra em uma fila (Task Queue ou Microtask Queue). O Event Loop verifica constantemente se a pilha de chamadas está vazia; se estiver, ele pega a primeira tarefa da fila e a executa.
\n\n// Exemplo simples que demonstra isso\nconsole.log('1. Início');\n\nsetTimeout(() => {\n console.log('3. Timer disparou');\n}, 0);\n\nPromise.resolve().then(() => {\n console.log('2. Microtask executada');\n});\n\nconsole.log('4. Fim');\n\n// Saída: 1, 4, 2, 3\n\nObserve que a Promise (microtask) foi executada antes do setTimeout (macrotask), mesmo com o timer tendo atraso de 0 ms. Esse é um conhecimento importante para a otimização em TypeScript.
3. Async/Await — o açúcar que muda tudo
\n\nCom o surgimento do ES2017, o mundo JS se dividiu entre "antes" e "depois". Async/await é um açúcar sintático sobre as Promises, que torna o código assíncrono semelhante ao síncrono. Ele não adiciona threads, mas aumenta radicalmente a legibilidade.
\n\n// Função que simula um atraso\nfunction delay(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n// Função assíncrona usando async/await\nasync function fetchUserData(userId: number): Promise<string> {\n console.log(`Solicitando dados do usuário ${userId}...`);\n await delay(2000); // Simula uma requisição de rede\n return `Dados do usuário ${userId}`;\n}\n\nasync function main() {\n console.log('Início');\n const user1 = await fetchUserData(1);\n console.log(