Worker Threads e Atomics API em JavaScript são a resposta quando uma tarefa pesada precisa rodar sem travar a tela. O padrão é dividir o dado entre os núcleos, apontar cada worker para um trecho de memória compartilhada e sincronizar o progresso com operações atômicas.
Worker Threads e Atomics API: o que você precisa saber
Worker Threads são APIs do JavaScript executado em ambientes como navegadores e Node.js para rodar código em segundo plano sem bloquear a thread principal. A Web Workers API existe desde 2009 e é suportada por praticamente 100% dos ambientes que executam JavaScript hoje. Atomics é a API que coordena o acesso a memória compartilhada entre essas threads.
O MDN descreve a Web Workers API como parte do padrão da plataforma web, e a página de Atomics lista os métodos que evitam condições de corrida. A questão prática é quando usar cada uma dessas peças.
Não existe um único caminho de thread em JavaScript. A linguagem foi projetada para executar um trecho de código por vez, o que reduz a complexidade de acesso aos dados. O ambiente de execução é quem expõe APIs internas para criar programas que rodam em paralelo.
Essa distinção evita um erro comum: dizer que JavaScript sempre roda em uma única thread de CPU. A thread principal de fato executa o runtime do JavaScript, mas workers adicionam linhas de execução paralelas. Código síncrono na thread principal continua bloqueando a interface, enquanto tarefas delegadas a workers não.
Vale notar que executar um código por vez não significa executar uma tarefa por vez. Você pode agendar trabalho assíncrono, bater em uma API HTTP e seguir com outras operações até a resposta chegar.
Se você trabalha com leitura de arquivos grandes, processamento de imagem, áudio, vídeo ou cálculos pesados, a divisão do trabalho entre workers muda a experiência de uso. O usuário continua clicando e navegando enquanto o processamento segue em segundo plano.
Por que rodar tudo na thread principal trava a interface?
A thread principal de um navegador é responsável por executar código JavaScript, reagir a eventos e atualizar a tela. Quando uma tarefa longa ocupa essa thread, o navegador não consegue repintar a página nem responder a cliques. O resultado é a interface congelada, que o usuário percebe como lentidão.
Aplicativos móveis nativos já usam há anos um modelo com pelo menos duas threads: uma dedicada à interface e outra ao processamento de dados. Enquanto uma thread renderiza a tela, a outra calcula, busca informações ou executa loops. Esse padrão evita que a tela pare.
No navegador, a maioria dos projetos concentra tudo na thread principal. Imagine baixar um arquivo de texto e tentar inspecionar linha a linha, filtrar registros ou fazer cálculos. Até o processamento terminar, a página fica irresponsiva e o usuário não consegue nem clicar em um botão.
Worker Threads deveriam fazer parte do manual de quem desenvolve para a web. Não se trata de uma técnica exótica, mas de uma prática básica que pode ser adotada desde o início de qualquer projeto com processamento pesado.
Como dividir uma imagem entre os núcleos da máquina
Dividir uma imagem entre os núcleos da máquina significa repartir a massa de pixels entre workers, um por CPU disponível. Cada worker recebe um intervalo do buffer compartilhado, aplica a transformação naquele trecho e sinaliza o progresso. O resultado é processamento paralelo sem cópias desnecessárias de memória.
O exemplo analisado aqui é um experimento de edição de imagens que roda no navegador. O cliente envia uma imagem grande; o código quebra a imagem em pequenos pedaços e delega cada pedaço a um worker individual. Cada worker processa sua porção e notifica a thread principal sobre o andamento.
Passo a passo do fluxo
- O worker recebe uma referência compartilhada da imagem por meio de um SharedArrayBuffer, não uma cópia dos pixels.
- Cada worker percorre o intervalo de pixels que lhe foi atribuído e aplica o filtro de escala de cinza.
- A cada pedaço concluído, o worker atualiza uma variável de progresso compartilhada usando Atomics.add.
- A thread principal lê esse progresso com Atomics.load e atualiza a barra na tela, sem travar a interface.
- Ao final, os resultados já estão no buffer compartilhado e a imagem reconstruída pode ser exibida ou baixada.
Essa estratégia serve para processar vídeo, áudio, imagem e outros dados diretamente na máquina do cliente. O processamento não depende de servidor, e o dado sensível não precisa ser enviado para fora do dispositivo do usuário.
SharedArrayBuffer e as cabeças de segurança do servidor
Para trabalhar com memória compartilhada em JavaScript, é preciso ativar o SharedArrayBuffer. O navegador exige que o servidor envie cabeçalhos HTTP específicos para habilitá-lo. Sem isso, a API fica indisponível e o projeto não funciona.
Os dois cabeçalhos responsáveis são Cross-Origin-Opener-Policy: same-origin e Cross-Origin-Embedder-Policy: require-corp. Eles isolam o contexto e reduzem riscos como ataques de temporização entre origens. A documentação da MDN sobre SharedArrayBuffer detalha essa exigência.
A consequência prática é que abrir a página HTML diretamente do disco, sem um servidor que envie esses cabeçalhos, não vai funcionar. Você precisa de um servidor local ou remoto configurado corretamente. No projeto usado como exemplo, um único pacote de desenvolvimento, o BrowserSync, cuida de servir os arquivos e recarregar a página a cada alteração.
SharedArrayBuffer não é o mesmo que Atomics. O primeiro define a memória que pode ser compartilhada entre workers; o segundo define como acessar essa memória sem corromper valores. Usar um sem o outro, em cenários de escrita concorrente, abre espaço para erros difíceis de reproduzir.
Quando usar a Atomics API e o que ela evita?
Use a Atomics API sempre que duas ou mais threads puderem acessar a mesma posição de memória compartilhada. Sem operações atômicas, uma thread pode ler um valor enquanto outra ainda o está modificando. O resultado é uma race condition, com valores desatualizados ou dados corrompidos.
Race conditions acontecem quando o resultado depende da ordem imprevisível em que as threads executam. Um contador de progresso, por exemplo, pode perder incrementos se dois workers lerem o mesmo valor antes de escrever. A operação Atomics.add evita isso ao ler e escrever de forma indivisível.
A tabela de métodos da Atomics API inclui operações como add, load, store, wait e notify. wait e notify permitem pausar uma thread até que uma condição de memória seja satisfeita, o que serve para sincronizar etapas entre workers.
Se duas threads tentarem atualizar o mesmo progresso ou o mesmo pixel, uma espera pela outra e o conflito não ocorre. É esse comportamento que garante a segurança de acesso, conhecida como thread safe. Sem Atomics, a coordenação fica por conta de código frágil e sujeito a falhas intermitentes.
O uso de operações atômicas costuma ter custo menor do que replicar dados. Evitar cópias de memória é o ponto central: se cada worker recebesse uma cópia própria da imagem, o consumo de memória cresceria muito e a máquina do usuário poderia ficar sobrecarregada. Em uma imagem de 8 MB, oito cópias somam 64 MB só de duplicação.
| Método | O que faz | Quando usar |
|---|---|---|
Atomics.add | Soma um valor a uma posição compartilhada | Contador de progresso atualizado por vários workers |
Atomics.load | Lê o valor atual da posição | Thread principal consultando o progresso |
Atomics.store | Escreve um valor na posição | Inicializar ou sobrescrever estado compartilhado |
Atomics.wait | Pausa a thread até a condição mudar | Sincronizar etapas entre workers |
Atomics.notify | Acorda threads em espera | Liberar workers quando o estado muda |
Quando você realmente precisa compartilhar memória
Compartilhar memória nem sempre é necessário. Se os workers não precisam alterar os mesmos dados, você pode usar apenas Worker Threads e enviar mensagens entre eles. Esse modelo funciona muito bem e evita toda a complexidade de sincronização.
A SharedArrayBuffer e a Atomics API entram quando o volume de dados é grande ou quando há uma única variável compartilhada, como o progresso de um processamento. Nesses casos, copiar o dado para cada thread desperdiça memória e tempo.
O critério é simples: se o dado cabe em uma mensagem sem custo relevante e não precisa de coordenação constante, use postMessage. Se o dado é grande, é uma imagem, um vídeo ou um buffer de pixels, e várias threads precisam atualizá-lo, a memória compartilhada compensa.
O próprio JavaScript foi desenhado para executar um código por vez. Isso reduz a complexidade de acesso aos dados e não é um defeito. Multithreading, quando entra, traz também os problemas clássicos de sistemas concorrentes. A escolha de compartilhar memória deve ser deliberada.
Como testar o projeto na sua máquina hoje
Você pode reproduzir o experimento clonando o repositório, instalando as dependências e iniciando o servidor de desenvolvimento. O projeto tem uma única dependência, usada para servir os arquivos com os cabeçalhos corretos e atualizar a página automaticamente.
Comandos
git clone <url-do-repositorio>
cd <pasta-do-projeto>
npm ci
npm start
O npm ci instala exatamente as versões travadas no package-lock.json, o que evita surpresas entre ambientes. O npm start inicia o BrowserSync, abre o navegador e aplica os cabeçalhos que habilitam o SharedArrayBuffer. Abrir o HTML diretamente no navegador não substitui esse passo.
Depois de iniciar, faça upload de uma imagem. Em uma máquina de seis núcleos, o navegador vai reportar seis threads disponíveis. A divisão do trabalho é feita automaticamente com base nesse número, e a barra de progresso indica o avanço do processamento.
O teste que aparece no vídeo usa uma imagem de 8 MB. Você também pode subir um arquivo maior para observar como o aumento do volume de pixels afeta o tempo total. O filtro de escala de cinza é só a operação de exemplo; a mesma estrutura serve para outros tipos de transformação por pixel.
Worker Threads substituem tudo? Limites e cuidados
Worker Threads não substituem a thread principal nem resolvem qualquer gargalo. Elas são indicadas quando existe trabalho paralelizável e pesado o suficiente para compensar o custo de criar workers e sincronizar resultados. Tarefas pequenas ou dependentes de ordem nem sempre se beneficiam.
A comunicação entre threads também tem custo. Enviar objetos grandes via postMessage copia os dados, o que pode anular o ganho. É por isso que o exemplo usa buffer compartilhado: os pixels ficam em um único lugar, e cada worker atua apenas sobre seu intervalo.
Erros de sincronização são difíceis de depurar porque dependem do tempo de execução. Um bug de race condition pode aparecer só em máquinas com mais núcleos ou sob carga. Ferramentas como Atomics reduzem o espaço para esse tipo de falha, mas exigem disciplina de quem escreve o código.
Multithreading também herda complexidade de sistemas concorrentes. Quem vem do mobile já conhece esse modelo; quem trabalha só com web costuma encontrá-lo pela primeira vez em cenários de processamento de mídia. Comece por casos pequenos antes de aplicar em produção.
Perguntas frequentes sobre threads e memória em JavaScript
O que é a Atomics API em JavaScript?
A Atomics API é um conjunto de métodos que executa operações de leitura e escrita em memória compartilhada de forma indivisível. Ela faz parte da especificação do JavaScript e é usada com SharedArrayBuffer para evitar race conditions entre threads. Sem ela, duas threads podem ler ou escrever o mesmo valor ao mesmo tempo.
JavaScript executa apenas uma coisa por vez?
A linguagem executa um trecho de código por vez, mas o ambiente de execução expõe APIs de paralelismo, como Worker Threads. Isso significa que uma tarefa pode rodar em segundo plano enquanto a thread principal continua atualizando a tela. A afirmação de que JavaScript só roda uma coisa de cada vez descreve um modelo antigo.
Quando preciso usar SharedArrayBuffer?
Você precisa de SharedArrayBuffer quando várias threads devem acessar os mesmos dados sem copiá-los, como um buffer de pixels de imagem ou vídeo. Se cada worker pode receber sua própria cópia sem custo relevante, o envio por postMessage é suficiente. A escolha depende do volume de dados e da frequência de atualização.
Por que o servidor precisa de cabeçalhos especiais para SharedArrayBuffer?
O navegador exige Cross-Origin-Opener-Policy e Cross-Origin-Embedder-Policy para habilitar memória compartilhada. Esses cabeçalhos isolam o contexto e reduzem ataques de temporização entre origens. Sem eles, a API fica indisponível e o projeto não inicia.
O que é uma race condition em JavaScript?
É quando o resultado depende da ordem imprevisível em que duas threads acessam o mesmo dado. Um contador de progresso pode perder incrementos se dois workers lerem o valor antes de gravar. A Atomics API resolve isso ao tornar cada operação indivisível.
Worker Threads funcionam em todos os navegadores?
A Web Workers API tem amplo suporte nos navegadores modernos, mas o SharedArrayBuffer depende dos cabeçalhos de isolamento e de contexto seguro. Verifique o suporte atual antes de assumir que a combinação funciona em qualquer ambiente. A MDN mantém a tabela de compatibilidade atualizada.
Preciso de servidor para processar imagem no navegador?
Não para o processamento em si. A imagem é lida no cliente, dividida entre workers e filtrada localmente, sem enviar os pixels para um servidor. Você só precisa de um servidor local que envie os cabeçalhos corretos durante o desenvolvimento.
Quanto de memória um buffer compartilhado economiza?
Em vez de criar uma cópia da imagem para cada worker, todos apontam para o mesmo buffer. Em uma imagem de 8 MB, isso evita até oito cópias em uma máquina com oito núcleos. O ganho cresce com o tamanho do arquivo e com o número de threads.
Vale a pena usar Worker Threads em projetos pequenos?
Vale quando existe uma tarefa pesada que travaria a interface, como conversão de arquivos ou cálculos longos. Para operações rápidas, o custo de criar e sincronizar workers pode superar o benefício. Meça o tempo antes de adotar a solução.
Como testar o projeto na minha máquina?
Clone o repositório, rode npm ci para instalar as dependências e npm start para subir o servidor. O navegador abre com os cabeçalhos necessários e você pode subir uma imagem para ver a divisão entre núcleos. Publicar a página como HTML estático não funciona.
Onde encontrar recursos em português sobre esses temas?
A Crazystack reúne formações e conteúdos sobre stacks de desenvolvimento, e a comunidade em torno do Gustavo Dev Doido publica materiais sobre JavaScript e TypeScript. Cursos como o Crazystack Typescript ajudam a fixar os fundamentos antes de partir para APIs mais avançadas, e iniciativas como o Bootcamp do Dev Doido trazem projetos guiados. Vale usar a Skala Blog para transformar vídeos técnicos em texto de apoio ao estudo.
Transforme o que você já sabe em texto que as pessoas encontram
A ideia central deste artigo é reaproveitar memória em vez de duplicar: threads leem o mesmo buffer, atualizam o mesmo progresso e evitam cópias inúteis. Você provavelmente tem explicações, entrevistas e aulas gravadas em vídeo que sofrem do problema oposto, presas em um formato que o buscador quase não indexa.
Com o Skala Blog, você cola a URL do vídeo, a ferramenta transcreve o conteúdo e gera um artigo estruturado a partir do que já foi dito. O conhecimento que está no YouTube passa a existir também em texto, pronto para ser encontrado por quem procura o tema.
Fork this article
Start a new branch from the same video, shaped your way. You keep the credit; the original keeps the attribution.
A fork in another language is filed as a translation of this article, so the two pages point at each other. You can unlink it later from the editor.
0/240
You are creating
- Format
- For
- Language
- Source
- Your angle
You will be asked to sign in before it is generated.
Buy credits