JavaScript Web Workers
Aprenda JavaScript Web Workers para executar código em uma thread em segundo plano — mantenha a UI responsiva durante tarefas pesadas, comunique-se com postMessage e transfira dados com objetos transferíveis.
JavaScript executa seu código em uma única thread principal — a mesma thread que organiza o layout da página, renderiza pixels e processa cliques e teclas. Essa thread única é o coração do event loop: ela pega uma tarefa, executa-a até o fim e então passa para a próxima. Assim, quando uma função faz algo genuinamente pesado — processar um array grande, analisar um arquivo de vários megabytes, calcular um hash de senha milhares de vezes — o loop fica preso dentro dessa função. Nada mais pode acontecer: a rolagem trava, os botões param de responder e a página parece congelada até que o trabalho termine.
Web Workers resolvem isso executando um script em uma thread separada em segundo plano, em paralelo com a thread principal. O trabalho pesado sai do caminho crítico, a UI permanece responsiva e as duas threads se comunicam passando mensagens.
Por Que Timers Não São Suficientes
O primeiro instinto mais comum é envolver o trabalho lento em um setTimeout e esperar que ele execute "em segundo plano". Não funciona assim. Os timers apenas adiama uma tarefa para uma rodada posterior do mesmo loop — quando o callback finalmente é chamado, ele ainda executa na thread principal e ainda bloqueia tudo enquanto é executado. (Veja agendamento com setTimeout e setInterval para entender como essa fila funciona de verdade.)
// This still freezes the page — it just freezes it 50ms later.
setTimeout(() => {
let total = 0;
for (let i = 0; i < 5_000_000_000; i++) total += i;
console.log(total);
}, 50);Um Web Worker é diferente: seu código executa em uma thread completamente diferente, então a thread principal fica livre para continuar renderizando e respondendo enquanto o worker trabalha.
Criando um Worker
Um worker é um arquivo JavaScript separado. Você cria um apontando o construtor Worker para a URL desse arquivo:
const worker = new Worker('worker.js');O navegador inicia uma nova thread, baixa worker.js e começa a executá-lo. A partir daí, os dois scripts se comunicam apenas através de mensagens — eles não compartilham variáveis, funções nem objetos.
Aqui está o menor par completo de arquivos.
main.js
const worker = new Worker('worker.js');
worker.postMessage('Hello from the main thread');
worker.onmessage = (event) => {
console.log('Main received:', event.data);
};worker.js
self.onmessage = (event) => {
console.log('Worker received:', event.data);
self.postMessage('Hello back from the worker');
};Comunicação com postMessage
A comunicação é bidirecional e assíncrona. A thread principal chama worker.postMessage(data) e escuta com worker.onmessage; dentro do worker, self.postMessage(data) envia de volta e self.onmessage recebe. Cada handler recebe um MessageEvent, e o conteúdo está na propriedade .data.
Os dados enviados são copiados, não compartilhados, usando o algoritmo de clone estruturado. Isso significa que você pode passar strings, números, booleanos, arrays, objetos simples, Map, Set, Date, ArrayBuffer e mais — mas não funções, nós do DOM ou instâncias de classe com métodos. Por ser uma cópia, alterar um objeto em um lado nunca afeta o outro.
Aqui está uma viagem de ida e volta completa. O worker calcula o n-ésimo número de Fibonacci com o algoritmo recursivo ingênuo — deliberadamente lento, exatamente o tipo de trabalho que travaria a UI se executasse na thread principal.
main.js
const worker = new Worker('worker.js');
worker.onmessage = (event) => {
console.log(`fib(${event.data.n}) = ${event.data.result}`);
};
// The page stays interactive while this runs on the worker thread.
worker.postMessage({ n: 42 });worker.js
function fib(n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
self.onmessage = (event) => {
const { n } = event.data;
const result = fib(n);
self.postMessage({ n, result });
};A thread principal dispara a requisição e imediatamente volta a lidar com cliques e renderização. Quando o worker termina, seu resultado chega como uma mensagem — sem travamento, sem oscilação de spinner.
O Escopo Global do Worker
Dentro de um worker não existe window. O objeto global é self (um DedicatedWorkerGlobalScope), e, crucialmente, não há document nem DOM. Um worker não pode ler ou alterar a página; se precisar atualizar a UI, ele envia uma mensagem e deixa a thread principal fazer isso.
Código dentro de um Web Worker não pode tocar o DOM. Não há document, nem window, nem acesso a elementos da página. Qualquer coisa visual deve ser devolvida à thread principal via postMessage. Essa restrição é o que torna os workers seguros para executar em paralelo — não há estado de UI compartilhado que possa ser corrompido.
Os workers estão longe de estar vazios, no entanto. O escopo do worker oferece muitas APIs úteis:
importScripts('a.js', 'b.js')para carregar scripts clássicos de forma síncrona.fetcheXMLHttpRequestpara requisições de rede.- Timers:
setTimeout,setInterval. console,crypto,TextEncoder/TextDecoder,WebSocket, IndexedDB e muito mais.
Isso torna os workers um ótimo lugar para busca de rede, análise, compressão e criptografia — trabalho autossuficiente que produz um resultado que você pode enviar de volta à página.
Tratando Erros e Encerrando
Se um worker lança um erro não capturado, ele aparece na thread principal através de worker.onerror:
worker.onerror = (event) => {
console.error(`Worker error: ${event.message} (${event.filename}:${event.lineno})`);
};Um worker continua executando até ser parado. Você pode pará-lo de fora com worker.terminate(), que encerra a thread imediatamente — qualquer trabalho em andamento é abandonado:
worker.terminate();Ou o worker pode se encerrar internamente quando terminar:
// inside worker.js
self.close();Encerrar workers ociosos libera memória; workers de longa duração que lidam com muitas mensagens podem ser mantidos sem problema.
Objetos Transferíveis: Mover em Vez de Copiar
O clone estruturado é conveniente, mas copia os dados. Para um payload binário grande — digamos, um buffer de imagem de 50 MB — copiar desperdiça memória e leva tempo. Objetos transferíveis permitem transferir a propriedade em vez de copiar: os dados são movidos para a outra thread sem cópia alguma, e o remetente perde o acesso a eles.
Você opta por isso passando um segundo argumento para postMessage — uma lista dos objetos a transferir:
const buffer = new ArrayBuffer(64 * 1024 * 1024); // 64 MB
// Transfer ownership of the buffer to the worker (no copy).
worker.postMessage({ buffer }, [buffer]);
console.log(buffer.byteLength); // 0 — this thread can no longer use itApós a transferência, buffer.byteLength é 0 no lado remetente: a memória agora pertence ao worker. Transferir é ideal para ArrayBuffers e os typed arrays construídos sobre eles — veja ArrayBuffer e arrays binários para saber como esses dados binários são estruturados. Outros transferíveis incluem MessagePort, ImageBitmap e OffscreenCanvas.
O conteúdo do segundo argumento também deve aparecer na mensagem. Em worker.postMessage({ buffer }, [buffer]), o buffer é referenciado pelo payload e listado como transferível. Listar algo que não é alcançável a partir da mensagem faz o navegador lançar um DataCloneError.
Workers com Módulos
Por padrão, um worker é um script clássico, por isso usa importScripts() em vez da sintaxe de módulo ES. Passe { type: 'module' } e o worker se torna um module worker que pode usar import estático e dinâmico:
const worker = new Worker('worker.js', { type: 'module' });worker.js
import { compress } from './compression.js';
self.onmessage = (event) => {
self.postMessage(compress(event.data));
};Module workers são o padrão moderno para código novo — eles oferecem imports adequados, modo estrito e um grafo de dependências mais limpo.
Outros Tipos de Workers
Um simples new Worker(...) cria um dedicated worker: ele pertence à única página que o criou. Existem dois tipos de worker relacionados — mas distintos — que você deve saber distinguir:
SharedWorker— uma única instância de worker compartilhada entre múltiplas abas, janelas ou iframes da mesma origem. As páginas se conectam a ele através de umMessagePort, o que o torna útil para coordenar estado ou uma única conexão de rede entre abas. Não é um dedicated worker mais rápido; é um worker compartilhado.- Service Worker — um worker especial que age como um proxy de rede, situando-se entre a página e a rede para habilitar cache, suporte offline e notificações push. É orientado a eventos e persiste além do tempo de vida da página. Esse é um trabalho diferente do "executar este cálculo fora da thread" de um dedicated worker; para saber mais sobre isso, leia Service Workers.
Regra geral: use um Web Worker dedicado para mover trabalho pesado de CPU para fora da thread principal, um SharedWorker para compartilhar um worker entre abas do mesmo site, e um Service Worker para controlar requisições de rede e criar apps com suporte offline.
Quando Usar Web Workers
Web Workers compensam sempre que uma tarefa é limitada pela CPU e longa o suficiente para ser percebida como travamento:
- Computação pesada — física, análise de dados, grandes ordenações e agregações.
- Processamento de imagens e vídeos, incluindo manipulação de pixels fora da tela.
- Análise e compressão de arquivos grandes (CSV, JSON, arquivos compactados).
- Criptografia — hashing e criptografar sem congelar a entrada.
- Processar grandes conjuntos de dados antes de devolver um resultado compacto para renderização.
Se o gargalo é aguardar a rede em vez de calcular, normalmente você não precisa de um worker — fetch já é assíncrono e não bloqueante. Workers brilham quando a própria CPU é o que mantém a thread principal ocupada.