Agora ficou mais fácil mover tarefas pesadas para linhas de execução em segundo plano com módulos JavaScript em web workers.
O JavaScript é de linha única, o que significa que ele só pode realizar uma operação por vez. Isso é intuitivo e funciona bem para muitos casos na Web, mas pode se tornar problemático quando precisamos realizar tarefas pesadas, como processamento, análise, computação ou análise de dados. À medida que mais aplicativos complexos são disponibilizados na Web, aumenta a necessidade de processamento multithread.
Na plataforma Web, a principal primitiva para linhas de execução e paralelismo é a API Web Workers. Os workers são uma abstração leve sobre threads do sistema operacional que expõem uma API de transmissão de mensagens para comunicação entre threads. Isso pode ser muito útil ao realizar cálculos caros ou operar em grandes conjuntos de dados, permitindo que a linha de execução principal seja executada sem problemas enquanto realiza as operações caras em uma ou mais linhas de execução em segundo plano.
Confira um exemplo típico de uso de worker, em que um script de worker fica aguardando mensagens da thread principal e responde enviando mensagens próprias:
page.js:
const worker = new Worker('worker.js');
worker.addEventListener('message', e => {
console.log(e.data);
});
worker.postMessage('hello');
worker.js:
addEventListener('message', e => {
if (e.data === 'hello') {
postMessage('world');
}
});
A API Web Worker está disponível na maioria dos navegadores há mais de dez anos. Isso significa que os workers têm excelente suporte a navegadores e são bem otimizados, mas também que são muito anteriores aos módulos JavaScript. Como não havia um sistema de módulos quando os workers foram projetados, a API para carregar código em um worker e compor scripts permaneceu semelhante às abordagens de carregamento de scripts síncronos comuns em 2009.
Histórico: trabalhadores clássicos
O construtor Worker usa um URL de script
clássico, que é
relativo ao URL do documento. Ele retorna imediatamente uma referência à nova instância do worker,
que expõe uma interface de mensagens, bem como um método terminate()
que interrompe e
destrói o worker imediatamente.
const worker = new Worker('worker.js');
Uma função importScripts()
está disponível nos web workers para carregar código adicional, mas ela pausa a execução do worker para buscar e avaliar cada script. Ele também executa scripts
no escopo global, como uma tag <script>
clássica. Isso significa que as variáveis em um script podem ser
substituídas pelas variáveis em outro.
worker.js:
importScripts('greet.js');
// ^ could block for seconds
addEventListener('message', e => {
postMessage(sayHello());
});
greet.js:
// global to the whole worker
function sayHello() {
return 'world';
}
Por isso, os web workers sempre tiveram um efeito grande na arquitetura de um aplicativo. Os desenvolvedores precisaram criar ferramentas e soluções alternativas inteligentes para usar web workers sem abrir mão das práticas de desenvolvimento modernas. Por exemplo, bundlers como
webpack incorporam uma pequena implementação de carregador de módulos no código gerado que usa importScripts()
para carregamento de código, mas envolve módulos em funções para evitar colisões de variáveis e simular
importações e exportações de dependências.
Inserir trabalhadores de módulo
Um novo modo para web workers com os benefícios de ergonomia e desempenho dos módulos
JavaScript está sendo lançado no Chrome 80, chamado de module workers. O construtor
Worker
agora aceita uma nova opção {type:"module"}
, que muda o carregamento e a
execução de scripts para corresponder a <script type="module">
.
const worker = new Worker('worker.js', {
type: 'module'
});
Como os workers de módulo são módulos JavaScript padrão, eles podem usar instruções de importação e exportação. Como em todos os módulos JavaScript, as dependências são executadas apenas uma vez em um determinado contexto (thread principal, worker etc.), e todas as importações futuras referenciam a instância do módulo já executada. O carregamento e a execução de módulos JavaScript também são otimizados pelos navegadores. As dependências de um módulo podem ser carregadas antes da execução dele, o que permite que árvores de módulos inteiras sejam carregadas em paralelo. O carregamento de módulos também armazena em cache o código analisado, o que significa que os módulos usados na thread principal e em um worker só precisam ser analisados uma vez.
A migração para módulos JavaScript também permite o uso de importação
dinâmica para carregamento lento de código sem bloquear a execução do
worker. A importação dinâmica é muito mais explícita do que usar importScripts()
para carregar dependências, já que as exportações do módulo importado são retornadas em vez de depender de variáveis globais.
worker.js:
import { sayHello } from './greet.js';
addEventListener('message', e => {
postMessage(sayHello());
});
greet.js:
import greetings from './data.js';
export function sayHello() {
return greetings.hello;
}
Para garantir um ótimo desempenho, o método importScripts()
antigo não está disponível em workers
de módulo. Mudar os workers para usar módulos JavaScript significa que todo o código é carregado no modo
estrito. Outra mudança
notável é que o valor de this
no escopo de nível superior de um módulo JavaScript é
undefined
, enquanto nos workers clássicos o valor é o escopo global do worker. Felizmente, sempre houve um self
global que fornece uma referência ao escopo global. Ele está disponível em todos os tipos de workers, incluindo service workers, e também no DOM.
Pré-carregar workers com modulepreload
Uma melhoria significativa de desempenho que vem com os service workers de módulo é a capacidade de pré-carregar
workers e as dependências deles. Com os service workers de módulo, os scripts são carregados e executados como módulos JavaScript padrão, o que significa que eles podem ser pré-carregados e até mesmo pré-analisados usando modulepreload
:
<!-- preloads worker.js and its dependencies: -->
<link rel="modulepreload" href="worker.js">
<script>
addEventListener('load', () => {
// our worker code is likely already parsed and ready to execute!
const worker = new Worker('worker.js', { type: 'module' });
});
</script>
Os módulos pré-carregados também podem ser usados pela linha de execução principal e pelos workers de módulo. Isso é útil para módulos importados nos dois contextos ou em casos em que não é possível saber com antecedência se um módulo será usado na linha de execução principal ou em um worker.
Antes, as opções disponíveis para pré-carregar scripts de web worker eram limitadas e não necessariamente confiáveis. Os workers clássicos tinham o próprio tipo de recurso "worker" para pré-carregamento, mas nenhum
navegador implementou <link rel="preload" as="worker">
. Como resultado, a principal técnica disponível para pré-carregar web workers era usar <link rel="prefetch">
, que dependia totalmente do cache HTTP. Quando usado em combinação com os cabeçalhos de cache corretos, isso permitiu evitar que a instanciação do worker precisasse esperar para baixar o script do worker. No entanto, ao contrário de
modulepreload
, essa técnica não oferecia suporte a dependências de pré-carregamento ou pré-análise.
E os trabalhadores compartilhados?
Os workers compartilhados foram atualizados com suporte para módulos JavaScript no Chrome 83. Assim como os trabalhadores dedicados, a construção de um trabalhador compartilhado com a opção {type:"module"}
agora carrega o script do trabalhador como um módulo, e não como um script clássico:
const worker = new SharedWorker('/worker.js', {
type: 'module'
});
Antes da compatibilidade com módulos JavaScript, o construtor SharedWorker()
esperava apenas um URL e um argumento name
opcional. Isso vai continuar funcionando para o uso clássico de workers compartilhados. No entanto,
criar workers compartilhados de módulo exige o uso do novo argumento options
. As opções disponíveis são as mesmas de um worker dedicado, incluindo a opção name
, que substitui o argumento name
anterior.
E o service worker?
A especificação do service worker já foi
atualizada para aceitar um
módulo JavaScript como ponto de entrada, usando a mesma opção {type:"module"}
dos workers de módulo.
No entanto, essa mudança ainda não foi implementada nos navegadores. Quando isso acontecer, será possível
instanciar um service worker usando um módulo JavaScript com o seguinte código:
navigator.serviceWorker.register('/sw.js', {
type: 'module'
});
Agora que a especificação foi atualizada, os navegadores estão começando a implementar o novo comportamento. Isso leva tempo porque há algumas complicações extras associadas à inclusão de módulos JavaScript no service worker. O registro do service worker precisa comparar scripts importados com as versões armazenadas em cache anteriores ao determinar se um update precisa ser acionado. Isso precisa ser implementado para módulos JavaScript quando usados para service workers. Além disso, os service workers precisam ignorar o cache para scripts em determinados casos ao verificar atualizações.