Voltar para o Blog

Web Workers: Como Usar Threads em JavaScript Para Melhorar Performance

Olá HaWkers, JavaScript é single-threaded por design. Isso significa que operações pesadas bloqueiam a thread principal, causando travamentos na interface. Web Workers resolvem esse problema permitindo executar código em threads separadas.

Vamos explorar como usar Web Workers para criar aplicações mais responsivas e performáticas.

O Problema da Thread Principal

A thread principal do JavaScript é responsável por:

  • Executar seu código
  • Processar eventos do DOM
  • Renderizar atualizações visuais
  • Responder a interações do usuário

Quando você executa código pesado, todas essas tarefas ficam bloqueadas:

// ❌ RUIM - Bloqueia a thread principal
function processarDadosPesados(dados) {
  // Simulação de processamento pesado
  const resultado = [];
  for (let i = 0; i < 10000000; i++) {
    resultado.push(Math.sqrt(dados[i % dados.length]) * Math.random());
  }
  return resultado;
}

// Durante essa execução, a UI fica congelada
// Cliques não respondem, animações param
const resultado = processarDadosPesados(meusDados);

Sintomas de thread principal bloqueada:

  • Interface travada
  • Animações pausadas
  • Inputs não respondem
  • Scroll engasgado
  • "Page Unresponsive" no navegador

Como Web Workers Funcionam

Web Workers executam JavaScript em uma thread separada, completamente isolada da thread principal.

Comunicação via Mensagens

Workers se comunicam com a thread principal através de mensagens:

// main.js - Thread principal
const worker = new Worker('worker.js');

// Enviar dados para o worker
worker.postMessage({ dados: meusDados, tipo: 'processar' });

// Receber resultado do worker
worker.onmessage = (event) => {
  console.log('Resultado:', event.data);
  // Atualizar UI com o resultado
  atualizarInterface(event.data);
};

// Tratar erros
worker.onerror = (error) => {
  console.error('Erro no worker:', error.message);
};
// worker.js - Thread separada
self.onmessage = (event) => {
  const { dados, tipo } = event.data;

  if (tipo === 'processar') {
    // Processamento pesado - não bloqueia a UI
    const resultado = processarDados(dados);

    // Enviar resultado de volta
    self.postMessage(resultado);
  }
};

function processarDados(dados) {
  const resultado = [];
  for (let i = 0; i < 10000000; i++) {
    resultado.push(Math.sqrt(dados[i % dados.length]) * Math.random());
  }
  return resultado;
}

Tipos de Workers

Dedicated Workers

O tipo mais comum. Exclusivo para um script específico:

// Criar worker dedicado
const worker = new Worker('worker.js');

// Encerrar worker quando não precisar mais
worker.terminate();

Shared Workers

Compartilhado entre múltiplas tabs/janelas da mesma origem:

// main.js
const sharedWorker = new SharedWorker('shared-worker.js');

sharedWorker.port.onmessage = (event) => {
  console.log('Mensagem recebida:', event.data);
};

sharedWorker.port.postMessage('Hello from tab');
// shared-worker.js
const connections = [];

self.onconnect = (event) => {
  const port = event.ports[0];
  connections.push(port);

  port.onmessage = (event) => {
    // Broadcast para todas as conexões
    connections.forEach(conn => {
      conn.postMessage(`Broadcast: ${event.data}`);
    });
  };

  port.start();
};

Service Workers

Para cache, push notifications e funcionalidade offline (não coberto em detalhes aqui).

Casos de Uso Práticos

1. Processamento de Imagens

// main.js
const imageWorker = new Worker('image-worker.js');

async function processarImagem(imageFile) {
  const bitmap = await createImageBitmap(imageFile);

  // Usar OffscreenCanvas para processar no worker
  const canvas = document.createElement('canvas');
  canvas.width = bitmap.width;
  canvas.height = bitmap.height;

  const offscreen = canvas.transferControlToOffscreen();

  // Transferir canvas para o worker (não copia, transfere)
  imageWorker.postMessage(
    { canvas: offscreen, bitmap },
    [offscreen, bitmap]
  );
}

imageWorker.onmessage = (event) => {
  if (event.data.tipo === 'progresso') {
    atualizarBarraProgresso(event.data.porcentagem);
  } else if (event.data.tipo === 'completo') {
    exibirResultado(event.data.imageData);
  }
};
// image-worker.js
self.onmessage = async (event) => {
  const { canvas, bitmap } = event.data;
  const ctx = canvas.getContext('2d');

  ctx.drawImage(bitmap, 0, 0);
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

  // Aplicar filtro (exemplo: grayscale)
  const pixels = imageData.data;
  for (let i = 0; i < pixels.length; i += 4) {
    const avg = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
    pixels[i] = avg;     // R
    pixels[i + 1] = avg; // G
    pixels[i + 2] = avg; // B

    // Reportar progresso
    if (i % 100000 === 0) {
      self.postMessage({
        tipo: 'progresso',
        porcentagem: Math.round((i / pixels.length) * 100)
      });
    }
  }

  ctx.putImageData(imageData, 0, 0);

  self.postMessage({
    tipo: 'completo',
    imageData: ctx.getImageData(0, 0, canvas.width, canvas.height)
  });
};

2. Parsing de Arquivos Grandes

// main.js
const csvWorker = new Worker('csv-worker.js');

async function processarCSV(file) {
  const text = await file.text();

  csvWorker.postMessage({ csv: text, opcoes: { delimiter: ',' } });
}

csvWorker.onmessage = (event) => {
  const { tipo, dados, erro } = event.data;

  if (tipo === 'sucesso') {
    console.log(`Processadas ${dados.length} linhas`);
    renderizarTabela(dados);
  } else if (tipo === 'erro') {
    console.error('Erro ao processar CSV:', erro);
  }
};
// csv-worker.js
self.onmessage = (event) => {
  try {
    const { csv, opcoes } = event.data;
    const linhas = csv.split('\n');
    const headers = linhas[0].split(opcoes.delimiter);

    const dados = [];
    for (let i = 1; i < linhas.length; i++) {
      if (!linhas[i].trim()) continue;

      const valores = linhas[i].split(opcoes.delimiter);
      const objeto = {};

      headers.forEach((header, index) => {
        objeto[header.trim()] = valores[index]?.trim();
      });

      dados.push(objeto);
    }

    self.postMessage({ tipo: 'sucesso', dados });
  } catch (erro) {
    self.postMessage({ tipo: 'erro', erro: erro.message });
  }
};

3. Cálculos Matemáticos Complexos

// main.js
const mathWorker = new Worker('math-worker.js');

function calcularEstatisticas(numeros) {
  return new Promise((resolve, reject) => {
    mathWorker.postMessage({ numeros });

    mathWorker.onmessage = (event) => {
      resolve(event.data);
    };

    mathWorker.onerror = (error) => {
      reject(error);
    };
  });
}

// Uso
const stats = await calcularEstatisticas(grandeArrayDeNumeros);
console.log('Média:', stats.media);
console.log('Desvio Padrão:', stats.desvioPadrao);
// math-worker.js
self.onmessage = (event) => {
  const { numeros } = event.data;

  // Média
  const soma = numeros.reduce((acc, n) => acc + n, 0);
  const media = soma / numeros.length;

  // Variância
  const somaQuadrados = numeros.reduce((acc, n) => {
    return acc + Math.pow(n - media, 2);
  }, 0);
  const variancia = somaQuadrados / numeros.length;

  // Desvio padrão
  const desvioPadrao = Math.sqrt(variancia);

  // Mediana
  const sorted = [...numeros].sort((a, b) => a - b);
  const meio = Math.floor(sorted.length / 2);
  const mediana = sorted.length % 2
    ? sorted[meio]
    : (sorted[meio - 1] + sorted[meio]) / 2;

  self.postMessage({
    media,
    mediana,
    variancia,
    desvioPadrao,
    min: Math.min(...numeros),
    max: Math.max(...numeros),
  });
};

Worker com TypeScript e Bundlers

Configuração com Vite

// src/workers/data-processor.worker.ts
self.onmessage = (event: MessageEvent<{ data: number[] }>) => {
  const { data } = event.data;
  const result = data.map(n => n * 2);
  self.postMessage(result);
};

export {}; // Necessário para TypeScript tratar como módulo
// src/main.ts
// Vite suporta workers nativamente com ?worker
import DataWorker from './workers/data-processor.worker?worker';

const worker = new DataWorker();

worker.postMessage({ data: [1, 2, 3, 4, 5] });

worker.onmessage = (event) => {
  console.log('Resultado:', event.data); // [2, 4, 6, 8, 10]
};

Configuração com Webpack

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.worker\.ts$/,
        use: {
          loader: 'worker-loader',
          options: {
            filename: '[name].[contenthash].worker.js',
          },
        },
      },
    ],
  },
};

Transferable Objects

Para dados grandes, use Transferable Objects para evitar cópias:

// main.js
const buffer = new ArrayBuffer(1024 * 1024 * 100); // 100MB

// ❌ Copia os dados (lento para dados grandes)
worker.postMessage({ buffer });

// ✅ Transfere ownership (instantâneo)
worker.postMessage({ buffer }, [buffer]);
// Após isso, `buffer` não está mais disponível na thread principal

Tipos Transferíveis

// ArrayBuffer
const arrayBuffer = new ArrayBuffer(1024);
worker.postMessage({ data: arrayBuffer }, [arrayBuffer]);

// MessagePort
const channel = new MessageChannel();
worker.postMessage({ port: channel.port2 }, [channel.port2]);

// ImageBitmap
const bitmap = await createImageBitmap(imageBlob);
worker.postMessage({ image: bitmap }, [bitmap]);

// OffscreenCanvas
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);

Pool de Workers

Para tarefas paralelas, use um pool de workers:

// worker-pool.js
class WorkerPool {
  constructor(workerScript, poolSize = navigator.hardwareConcurrency || 4) {
    this.workers = [];
    this.queue = [];
    this.activeWorkers = new Set();

    for (let i = 0; i < poolSize; i++) {
      const worker = new Worker(workerScript);
      worker.id = i;
      this.workers.push(worker);
    }
  }

  execute(data) {
    return new Promise((resolve, reject) => {
      const task = { data, resolve, reject };

      const availableWorker = this.workers.find(
        w => !this.activeWorkers.has(w.id)
      );

      if (availableWorker) {
        this.runTask(availableWorker, task);
      } else {
        this.queue.push(task);
      }
    });
  }

  runTask(worker, task) {
    this.activeWorkers.add(worker.id);

    worker.onmessage = (event) => {
      task.resolve(event.data);
      this.activeWorkers.delete(worker.id);
      this.processQueue();
    };

    worker.onerror = (error) => {
      task.reject(error);
      this.activeWorkers.delete(worker.id);
      this.processQueue();
    };

    worker.postMessage(task.data);
  }

  processQueue() {
    if (this.queue.length === 0) return;

    const availableWorker = this.workers.find(
      w => !this.activeWorkers.has(w.id)
    );

    if (availableWorker) {
      const task = this.queue.shift();
      this.runTask(availableWorker, task);
    }
  }

  terminate() {
    this.workers.forEach(w => w.terminate());
  }
}

// Uso
const pool = new WorkerPool('processor.worker.js', 4);

const results = await Promise.all([
  pool.execute({ chunk: data.slice(0, 1000) }),
  pool.execute({ chunk: data.slice(1000, 2000) }),
  pool.execute({ chunk: data.slice(2000, 3000) }),
  pool.execute({ chunk: data.slice(3000, 4000) }),
]);

Limitações dos Web Workers

O Que NÃO Está Disponível

// worker.js

// ❌ DOM não está disponível
document.getElementById('algo'); // Error

// ❌ window não existe
window.localStorage; // Error

// ❌ alert, confirm, prompt não existem
alert('teste'); // Error

// ✅ Mas você pode usar:
fetch('/api/data'); // OK
setTimeout(() => {}, 1000); // OK
console.log('teste'); // OK
crypto.randomUUID(); // OK
self.indexedDB; // OK

Workarounds Comuns

// Se precisar de dados do DOM, extraia antes de enviar ao worker

// main.js
const elementosData = Array.from(document.querySelectorAll('.item'))
  .map(el => ({
    texto: el.textContent,
    dataset: { ...el.dataset }
  }));

worker.postMessage({ elementos: elementosData });

Conclusão

Web Workers são uma ferramenta poderosa para manter suas aplicações responsivas. Use-os para:

  • Processamento de dados pesados
  • Parsing de arquivos grandes
  • Cálculos matemáticos complexos
  • Processamento de imagens
  • Qualquer operação que leve mais de 50ms

Lembre-se das melhores práticas:

  • Use Transferable Objects para dados grandes
  • Implemente pool de workers para tarefas paralelas
  • Trate erros adequadamente
  • Termine workers quando não precisar mais deles

Se você está interessado em mais técnicas de performance, recomendo conferir o artigo Debugging JavaScript: Técnicas Avançadas com DevTools onde exploramos como identificar gargalos de performance na sua aplicação.

Bora pra cima! 🦅

Comentários (0)

Esse artigo ainda não possui comentários 😢. Seja o primeiro! 🚀🦅

Adicionar comentário