W3docs

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.

Aviso

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.
  • fetch e XMLHttpRequest para 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 it

Apó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.

Nota

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 um MessagePort, 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.
Informação

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.

Teste Seu Conhecimento

Prática
O código executado dentro de um Web Worker pode acessar o DOM diretamente?
O código executado dentro de um Web Worker pode acessar o DOM diretamente?
Prática
Como os dados passados para worker.postMessage(data) chegam ao worker por padrão?
Como os dados passados para worker.postMessage(data) chegam ao worker por padrão?
Prática
Qual é a principal vantagem de transferir um ArrayBuffer com worker.postMessage(buffer, [buffer])?
Qual é a principal vantagem de transferir um ArrayBuffer com worker.postMessage(buffer, [buffer])?
Was this page helpful?