Como criar linhas de execução na Web com workers do módulo

Agora, mover trabalho pesado para linhas de execução em segundo plano é mais fácil com os módulos JavaScript em web workers.

O JavaScript tem linha de execução única, ou seja, ele só pode executar uma operação por vez. Isso é intuitiva e funciona bem em muitos casos na Web, mas pode se tornar problemática quando realizar tarefas trabalhosas, como processamento de dados, análise, computação ou análise. Conforme cada vez mais aplicativos complexos são entregues na Web, há uma necessidade crescente de uso processamento.

Na plataforma Web, o principal primitivo para linhas de execução e paralelismo é a classe Web API Workers do Google Cloud. Os workers são uma abstração leve sobre o sistema operacional linhas de execução que expõem uma API de transmissão de mensagens para comunicação entre linhas de execução. Isso pode ser extremamente útil ao realizar cálculos dispendiosos ou em grandes conjuntos de dados, permitindo que a linha de execução principal funcione sem problemas enquanto executa operações caras em uma ou mais linhas de execução em segundo plano.

Este é um exemplo típico de uso de worker, em que um script de worker detecta mensagens do worker de linha de execução 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. Embora isso significa que os funcionários têm um excelente suporte a navegadores e estão bem otimizados, isso também significa que eles antes dos módulos JavaScript. Como não havia sistema de módulos quando os workers foram criados, a API para carregar código em um worker e escrever scripts permaneceu semelhante ao script síncrono de carregamento de dados comuns em 2009.

Histórico: workers clássicos

O construtor Worker usa um objeto clássico script, que é em relação ao URL do documento. Ela retorna imediatamente uma referência para a nova instância de worker, que expõe uma interface de mensagens, bem como um método terminate(), que para imediatamente e destrói o worker.

const worker = new Worker('worker.js');

Uma função importScripts() está disponível nos web workers para carregar código adicional, mas 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, o que significa que as variáveis em um script podem ser substituídos pelas variáveis de 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 essa razão, os web workers sempre impuseram um efeito maior na arquitetura de um para o aplicativo. Os desenvolvedores tiveram que criar ferramentas e soluções alternativas inteligentes para tornar possível usar workers da Web sem abrir mão das práticas modernas de desenvolvimento. Por exemplo, bundlers gostam O webpack incorpora uma pequena implementação de carregador de módulo ao 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ência.

Inserir workers do módulo

Um novo modo para workers da Web com os benefícios de ergonomia e desempenho do JavaScript módulos são enviados no Chrome 80, chamados de workers de módulo. A O construtor Worker agora aceita uma nova opção {type:"module"}, que muda o carregamento do script e 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. Conforme com todos os módulos JavaScript, as dependências são executadas apenas uma vez em um determinado contexto (linha de execução principal, worker etc.), e todas as importações futuras fazem referência à instância do módulo já executada. O carregamento e a execução de módulos JavaScript também é otimizada pelos navegadores. As dependências de um módulo podem ser carregado antes do módulo ser executado, o que permite que árvores inteiras do módulo sejam carregadas em paralelo. O carregamento do módulo também armazena em cache o código analisado, ou seja, os módulos usados na interface principal linha de execução e em um worker só precisam ser analisados uma vez.

A mudança para módulos JavaScript também permite o uso de recursos dinâmicos import para código de carregamento lento sem bloquear a execução do o 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 dependerem 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 no módulo. trabalhadores Alternar os workers para que usem módulos JavaScript significa que todo o código é carregado em estrito . Outra mudança notável é que o valor de this no escopo de nível superior de um módulo JavaScript é undefined, enquanto que nos workers clássicos o valor é o escopo global do worker. Felizmente, há sempre foi um self global que fornece uma referência ao escopo global. Ela está disponível no todos os tipos de workers, inclusive service workers e no DOM.

Pré-carregar workers com modulepreload

Uma melhoria substancial no desempenho dos workers dos módulos é a capacidade de pré-carregar os workers e as dependências deles. Com os workers do módulo, os scripts são carregados e executados como padrão Módulos JavaScript, 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 do módulo. Isso é útil para módulos importados em ambos os contextos ou nos 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 são necessariamente confiáveis. Os workers clássicos tinham seu próprio "trabalhador" tipo de recurso para pré-carregamento, mas não navegadores implementados <link rel="preload" as="worker">. Como resultado, a técnica principal disponível para pré-carregamento de web workers era usar <link rel="prefetch">, que dependia exclusivamente no cache HTTP. Quando usada em combinação com os cabeçalhos de cache corretos, isso tornou possível para evitar que a instanciação do worker tenha que esperar para fazer o download do script dele. No entanto, ao contrário modulepreload essa técnica não oferece suporte ao pré-carregamento de dependências ou à preparação.

E os workers compartilhados?

Os workers compartilhados têm foi atualizada com suporte para módulos JavaScript a partir do Chrome 83. Como os workers dedicados, a criação de um worker compartilhado com a opção {type:"module"} agora carrega o script dele como uma em vez de um script clássico:

const worker = new SharedWorker('/worker.js', {
  type: 'module'
});

Antes do suporte a módulos JavaScript, o construtor SharedWorker() esperava apenas uma URL e um argumento name opcional. Isso continuará funcionando para o uso de workers compartilhados clássicos. porém para criar workers compartilhados do módulo, é necessário usar o novo argumento options. Os recursos disponíveis opções são as mesmas de um worker dedicado, incluindo a opção name, que substitui argumento name anterior.

E quanto ao service worker?

A especificação do service worker já foi foi atualizado para oferecer suporte ao módulo JavaScript como ponto de entrada, usando a mesma opção {type:"module"} que os workers do módulo. No entanto, essa mudança ainda não foi implementada nos navegadores. Quando isso acontecer, será possível para 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 adicionais associadas ao uso do JavaScript módulos para service worker. O registro de service workers precisa comparar scripts importados com as versões anteriores armazenadas em cache quando determinar se uma atualização deve ser acionada, e isso precisa ser implementado em módulos JavaScript, quando usadas por service workers. Além disso, os service workers precisam ser capazes de ignorar a cache de scripts em certos casos quando para verificar se há atualizações.

Outros recursos e leituras