O que Todo Dev Deve Saber sobre Web Workers: Paralelismo Real no Navegador com JavaScript Já leu

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

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.

Referências


Artigos relacionados