Web Workers: Paralelismo Real no Navegador com JavaScript
Web Workers representam uma das funcionalidades mais poderosas do JavaScript moderno para resolver um problema clássico: o bloqueio da thread principal. Ao contrário do que muitos acreditam, JavaScript não é naturalmente single-threaded no navegador — você tem a capacidade de criar threads genuinamente paralelas através da API Web Workers. Neste artigo, você aprenderá não apenas como funcionam, mas também quando e por que usá-los.
Por que Web Workers importam
A thread principal do navegador é responsável por renderização, manipulação do DOM e execução de scripts. Uma operação pesada (cálculos complexos, processamento de grandes volumes de dados) bloqueia tudo isso, congelando a interface. Web Workers executam código em uma thread separada, não bloqueando a UI. Esse paralelismo real permite que seu aplicativo permaneça responsivo mesmo durante operações intensivas.
Fundamentos: Como Web Workers Funcionam
Arquitetura e comunicação
Web Workers operam em um modelo de "compartilhamento zero". O worker e a thread principal não compartilham memória diretamente; comunicam-se apenas através de mensagens. Isso é uma segurança, não uma limitação — evita race conditions e deadlocks. O worker executa um arquivo JavaScript isolado e não tem acesso ao DOM, à janela ou ao localStorage.
A comunicação ocorre via postMessage() e o evento message. Dados são copiados (structured clone), não referenciados. Para dados grandes, você pode transferir a propriedade usando Transferable Objects, eliminando a cópia.
Criando seu primeiro worker
Comece com dois arquivos: a página principal e o arquivo do worker.
Arquivo principal (main.js):
// Criar uma instância do worker
const worker = new Worker('worker.js');
// Enviar mensagem para o worker
worker.postMessage({ numero: 1000000 });
// Receber resultado
worker.onmessage = (event) => {
const resultado = event.data;
console.log('Resultado recebido:', resultado);
document.getElementById('resultado').textContent = resultado;
};
// Tratamento de erro
worker.onerror = (error) => {
console.error('Erro no worker:', error.message);
};
Arquivo do worker (worker.js):
// Receber mensagem da thread principal
self.onmessage = (event) => {
const numero = event.data.numero;
// Operação pesada
let soma = 0;
for (let i = 0; i < numero; i++) {
soma += Math.sqrt(i);
}
// Enviar resultado de volta
self.postMessage({ resultado: soma, tempo: Date.now() });
};
Esse exemplo demonstra o padrão básico. O worker executa em paralelo, mantendo a UI responsiva. Teste abrindo o console: a página não congela durante o cálculo.
Casos de Uso Práticos e Padrões Avançados
Processamento de imagens
Um caso real: aplicar filtros a imagens grandes. Sem workers, a UI congela. Com workers, o processamento é transparente ao usuário.
// main.js
const worker = new Worker('imageProcessor.js');
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Transferir o buffer sem copiar (Transferable Object)
worker.postMessage(
{ imageData: imageData, filter: 'grayscale' },
[imageData.data.buffer] // Transferir propriedade do buffer
);
worker.onmessage = (event) => {
const processedData = event.data.imageData;
ctx.putImageData(processedData, 0, 0);
};
// imageProcessor.js
self.onmessage = (event) => {
const { imageData, filter } = event.data;
const data = imageData.data;
if (filter === 'grayscale') {
for (let i = 0; i < data.length; i += 4) {
const gray = data[i] * 0.299 + data[i+1] * 0.587 + data[i+2] * 0.114;
data[i] = data[i+1] = data[i+2] = gray;
}
}
self.postMessage({ imageData: imageData }, [imageData.data.buffer]);
};
Pool de workers
Para múltiplas tarefas, um pool reutiliza workers, evitando overhead de criação.
class WorkerPool {
constructor(scriptPath, poolSize = 4) {
this.workers = [];
this.taskQueue = [];
for (let i = 0; i < poolSize; i++) {
const worker = new Worker(scriptPath);
worker.busy = false;
worker.onmessage = (e) => this.handleWorkerResult(worker, e);
this.workers.push(worker);
}
}
executeTask(data) {
return new Promise((resolve, reject) => {
const task = { data, resolve, reject };
const availableWorker = this.workers.find(w => !w.busy);
if (availableWorker) {
this.runTask(availableWorker, task);
} else {
this.taskQueue.push(task);
}
});
}
runTask(worker, task) {
worker.busy = true;
worker.currentTask = task;
worker.postMessage(task.data);
}
handleWorkerResult(worker, event) {
const { resolve } = worker.currentTask;
resolve(event.data);
worker.busy = false;
if (this.taskQueue.length > 0) {
this.runTask(worker, this.taskQueue.shift());
}
}
}
// Uso
const pool = new WorkerPool('task.js', 4);
Promise.all([
pool.executeTask({ numero: 1000000 }),
pool.executeTask({ numero: 2000000 }),
pool.executeTask({ numero: 3000000 })
]).then(results => console.log(results));
Limitações e Considerações Críticas
O que um worker NÃO pode fazer
Web Workers não acessam o DOM, window, parent ou document. Não podem usar alert() ou manipular elementos HTML. Essa restrição existe porque o DOM é single-threaded por design — threads concorrentes poderiam gerar inconsistências visuais.
Shared Workers e Service Workers são variações que resolvem casos específicos: Shared Workers compartilham estado entre abas; Service Workers funcionam offline. Não confunda com Web Workers simples — cada instância de Web Worker é isolada.
Performance e quando NOT usar
Criar um worker tem custo: parse do JavaScript, setup de thread. Para operações rápidas (< 50ms), o overhead supera o benefício. Use workers apenas para tarefas pesadas e assíncronas. Debugging é mais complexo — a maioria dos browsers oferece suporte, mas o inspector de workers é menos desenvolvido que das páginas normais.
Conclusão
Web Workers são essenciais para aplicações JavaScript sérias. Você aprendeu: (1) como criar e comunicar com workers através de postMessage() sem compartilhar memória; (2) padrões práticos como processamento de imagens e pools de workers para escalabilidade; (3) limitações críticas — sem acesso ao DOM, mas com paralelismo genuíno garantido. O próximo passo é experimentar com seus próprios projetos: processamento de dados, criptografia, análise de arquivos grandes.