Concorrência e Multithreading em JavaScript: como fazer o código rodar mais rápido
JavaScript historicamente foi uma linguagem single-thread, mas isso não significa que ela não pode processar várias tarefas simultaneamente. No Node.js moderno e nos navegadores, existem mecanismos poderosos para trabalhar com concorrência e multithreading. Compreender esses conceitos é criticamente importante para criar aplicações server-side e client-side de alto desempenho.
Neste artigo, vamos entender a diferença entre concorrência e paralelismo, como funciona o Event Loop, quando usar Worker Threads e Web Workers, além de analisar exemplos práticos em Node.js.
1. Event Loop e I/O não bloqueante
A base da concorrência em JavaScript é o Event Loop (loop de eventos). Este é um mecanismo que permite executar operações assíncronas (por exemplo, leitura de arquivos, requisições ao banco de dados) sem bloquear a thread principal.
Imagine uma fila: o Event Loop pega uma tarefa da fila (call stack), a executa e, se encontrar uma operação assíncrona (como setTimeout ou fetch), a transfere para a Web API (no navegador) ou libuv (no Node.js). Após a conclusão da operação, seu callback vai para a fila de tarefas (task queue). O Event Loop verifica constantemente se a pilha não está vazia e, se estiver vazia, pega a próxima tarefa da fila.
Exemplo de código assíncrono:
const fs = require('fs');
console.log('Início');
fs.readFile('example.txt', 'utf8', (err, data) => { if (err) throw err; console.log('Arquivo lido:', data);});
console.log('Fim');
// Saída:// Início// Fim// Arquivo lido: ...Observe: console.log('Fim') é executado antes da leitura do arquivo. Isso é I/O não bloqueante — a thread principal não espera a conclusão da operação, mas continua a execução.
2. Web Workers e Worker Threads: verdadeiro multithreading
Embora o Event Loop lide bem com operações de I/O, ele não ajuda na execução de cálculos "pesados" (processamento de imagens, criptografia, ordenações complexas). Se você executar esse tipo de código na thread principal, toda a interface (ou servidor) "congelará" até a conclusão do cálculo.
Para resolver esse problema, existem os Web Workers (no navegador) e Worker Threads (no Node.js). Eles permitem executar código JavaScript em threads separadas do sistema operacional.
Exemplo com Worker Threads no Node.js
Vamos criar o arquivo worker.js:
const { parentPort } = require('worker_threads');
// Tarefa pesada: calcular a soma dos númeroslet sum = 0;for (let i = 0; i < 1e9; i++) { sum += i;}
// Envia o resultado de volta para a thread principalparentPort.postMessage(sum);Arquivo principal main.js:
const { Worker } = require('worker_threads');
console.log('Thread principal: iniciando worker');
const worker = new Worker('./worker.js');
worker.on('message', (result) => { console.log('Resultado do worker:', result);});
worker.on('error', (err) => { console.error('Erro no worker:', err);});
worker.on('exit', (code) => { if (code !== 0) { console.error(`Worker encerrado com código ${code}`); }});
console.log('Thread principal: continuando trabalho...');
// Saída:// Thread principal: iniciando worker// Thread principal: continuando trabalho...// Resultado do worker: 499999999500000000Como você pode ver, a thread principal não é bloqueada, e o worker executa os cálculos em segundo plano. Isso é o verdadeiro multithreading.
3. Async/Await e Promise: gerenciamento de concorrência
O JavaScript moderno oferece maneiras elegantes de trabalhar com código assíncrono — async/await e Promise. Eles não criam novas threads, m