Thread sul Web con i worker dei moduli

Spostare il lavoro pesante nei thread in background è ora più facile con i moduli JavaScript nei web worker.

JavaScript è a thread singolo, il che significa che può eseguire una sola operazione alla volta. Questo è intuitiva e funziona bene in molti casi sul web, ma può diventare problematico quando dobbiamo eseguire attività gravose come l'elaborazione, l'analisi, il calcolo o l'analisi dei dati. Sempre più spesso di applicazioni complesse vengono distribuite sul web, aumenta la necessità di strumenti multi-thread e l'elaborazione dei dati.

Sulla piattaforma web, la primitiva principale per i thread e il parallelismo è Web API Workers. I worker sono un'astrazione leggera che si aggiunge al sistema operativo thread che espongono un'API che trasmette i messaggi per le comunicazioni tra thread. Questo può essere enormemente utile quando si eseguono calcoli costosi o operando su set di dati di grandi dimensioni, consentendo l'esecuzione fluida del thread principale durante l'esecuzione per le operazioni più costose su uno o più thread in background.

Ecco un tipico esempio di utilizzo dei worker, in cui uno script worker ascolta i messaggi dall'istanza principale thread e risponde inviando i propri messaggi:

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');
  }
});

L'API Web Worker è disponibile nella maggior parte dei browser da oltre dieci anni. Anche se significa che i lavoratori hanno un eccellente supporto per il browser e sono ben ottimizzati, significa anche precedenti a moduli JavaScript. Poiché al momento della progettazione dei worker non esisteva un sistema di moduli, l'API per il caricamento di codice in un worker e la composizione di script è rimasta simile allo script sincrono gli approcci al caricamento più comuni comuni nel 2009.

Cronologia: worker classici

Il costruttore worker prende un script, che è rispetto all'URL del documento. Restituisce immediatamente un riferimento alla nuova istanza worker, che espone un'interfaccia di messaggistica, nonché un metodo terminate() che si interrompe immediatamente distrugge il worker.

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

Nei web worker è disponibile una funzione importScripts() per caricare codice aggiuntivo, ma mette in pausa l'esecuzione del worker per recuperare e valutare ogni script. Esegue anche script nell'ambito globale come un tag <script> classico, il che significa che le variabili in uno script possono essere sovrascritto dalle variabili in un'altra.

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';
}

Per questo motivo, i web worker da sempre hanno imposto un impatto enorme sull'architettura di un un'applicazione. Gli sviluppatori hanno dovuto creare strumenti intelligenti e soluzioni alternative per consentire usano i web worker senza rinunciare alle moderne pratiche di sviluppo. Ad esempio, i bundler come webpack incorpora una piccola implementazione del caricatore di moduli nel codice generato che utilizza importScripts() per il caricamento del codice, ma aggrega i moduli nelle funzioni per evitare collisioni variabili importazioni ed esportazioni delle dipendenze.

Inserisci i worker del modulo

Una nuova modalità per chi lavora sul web con i vantaggi di JavaScript in termini di ergonomia e prestazioni moduli saranno disponibili in Chrome 80, chiamato worker dei moduli. La Il costruttore Worker ora accetta una nuova opzione {type:"module"}, che modifica il caricamento e esecuzione corrispondente a <script type="module">.

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

Poiché i worker dei moduli sono moduli JavaScript standard, possono utilizzare istruzioni di importazione ed esportazione. Come con tutti i moduli JavaScript, le dipendenze vengono eseguite una sola volta in un determinato contesto (thread principale, worker e così via) e tutte le importazioni future faranno riferimento all'istanza del modulo già eseguita. Il caricamento e l'esecuzione dei moduli JavaScript è inoltre ottimizzata dai browser. Le dipendenze di un modulo possono essere caricati prima dell'esecuzione del modulo, il che consente il caricamento di intere strutture di moduli parallelo. Il caricamento del modulo memorizza inoltre nella cache il codice analizzato, il che significa che i moduli utilizzati sulla piattaforma thread e in un worker devono essere analizzati una sola volta.

Il passaggio ai moduli JavaScript consente inoltre l'uso di di caricamento per il caricamento lento di codice senza bloccare l'esecuzione dei del worker. L'importazione dinamica è molto più esplicita rispetto all'utilizzo di importScripts() per caricare le dipendenze, poiché le esportazioni del modulo importato vengono restituite anziché fare affidamento su variabili globali.

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;
}

Per garantire prestazioni ottimali, il metodo importScripts() precedente non è disponibile all'interno del modulo worker. Il passaggio ai worker per l'utilizzo dei moduli JavaScript implica il caricamento di tutto il codice in modo rigoroso . Un altro una modifica significativa è che il valore this nell'ambito di primo livello di un modulo JavaScript undefined, mentre nei worker classici il valore è l'ambito globale del worker. Fortunatamente, è sempre stato un valore self globale che fornisce un riferimento all'ambito globale. È disponibile in tutti i tipi di worker, inclusi i Service worker, nonché nel DOM.

Precarica i worker con modulepreload

Un miglioramento sostanziale delle prestazioni offerto dai worker dei moduli è la capacità di precaricare e le loro dipendenze. Con i worker del modulo, gli script vengono caricati ed eseguiti come standard Moduli JavaScript, il che significa che possono essere precaricati e persino pre-analizzati utilizzando 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>

I moduli precaricati possono essere utilizzati anche dal thread principale e dai worker del modulo. Questo è utile per di moduli importati in entrambi i contesti o nei casi in cui non sia possibile conoscere in anticipo se un modulo verrà utilizzato nel thread principale o in un worker.

In precedenza, le opzioni disponibili per il precaricamento degli script dei worker web erano limitate e non necessariamente affidabili. I lavoratori classici avevano il loro "lavoratore" il tipo di risorsa per il precaricamento, ma browser ha implementato <link rel="preload" as="worker">. Di conseguenza, la tecnica principale per il precaricamento dei web worker era l'utilizzo di <link rel="prefetch">, che si basava interamente nella cache HTTP. L'utilizzo in combinazione con le intestazioni di memorizzazione nella cache corrette ha reso possibile per evitare che l'istanza worker debba attendere per scaricare lo script worker. Tuttavia, a differenza di modulepreload questa tecnica non supportava il precaricamento o il pre-analisi.

E i lavoratori condivisi?

I lavoratori condivisi hanno è stato aggiornato con il supporto dei moduli JavaScript a partire da Chrome 83. Come per i lavoratori dedicati, creando un worker condiviso con l'opzione {type:"module"} ora carica lo script worker anziché uno script classico:

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

Prima del supporto dei moduli JavaScript, il costruttore SharedWorker() prevedeva solo una e un argomento name facoltativo. Questa modalità continuerà a funzionare per l'utilizzo classico dei worker condivisi. tuttavia Per creare i worker condivisi dei moduli è necessario utilizzare il nuovo argomento options. Il opzioni sono gli stessi di un worker dedicato, inclusa l'opzione name che ha la precedenza l'argomento name precedente.

E il service worker?

La specifica del service worker è già stata aggiornato per supportare l'accettazione di modulo JavaScript come punto di ingresso, utilizzando la stessa opzione {type:"module"} dei worker del modulo. tuttavia questa modifica deve essere ancora implementata nei browser. Dopodiché sarà possibile per creare un'istanza di un service worker usando un modulo JavaScript usando il seguente codice:

navigator.serviceWorker.register('/sw.js', {
  type: 'module'
});

Ora che la specifica è stata aggiornata, i browser stanno iniziando a implementare il nuovo comportamento. Questa operazione richiede tempo perché ci sono alcune complicazioni aggiuntive associate all'introduzione di JavaScript ai Service worker. La registrazione del Service worker deve confrontare gli script importati con le precedenti versioni memorizzate nella cache, determinare se attivare o meno un aggiornamento, che deve essere implementato per i moduli JavaScript quando sono utilizzati per i service worker. Inoltre, i Service worker devono essere in grado di aggirare per gli script in alcuni casi, quando la verifica della disponibilità di aggiornamenti.

Risorse aggiuntive e ulteriori approfondimenti