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 principalTipos 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; // OKWorkarounds 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.

