Exécuter des threads Web avec des nœuds de calcul de module

Il est désormais plus facile d'effectuer des tâches lourdes dans des threads en arrière-plan grâce aux modules JavaScript des nœuds de calcul Web.

JavaScript est monothread, ce qui signifie qu'il ne peut effectuer qu'une seule opération à la fois. C'est intuitif et efficace dans de nombreux cas sur le Web, mais cela peut s'avérer problématique lorsque qui effectuent des tâches lourdes telles que le traitement, l'analyse, le calcul ou l'analyse de données. Alors que de plus en plus des applications complexes sont livrées sur le Web, le besoin d'utiliser des modèles multithread en cours de traitement.

Sur la plate-forme Web, la principale primitive du threading et du parallélisme est la couche Web API Workers. Les nœuds de calcul sont une abstraction légère au-dessus du système d'exploitation threads qui exposent une API de transmission de messages pour la communication inter-threads. Cela peut s'avérer extrêmement utile lors de l'exécution de calculs coûteux ou opèrent sur des ensembles de données volumineux, ce qui permet au thread principal de fonctionner correctement des opérations coûteuses sur un ou plusieurs threads en arrière-plan.

Voici un exemple typique d'utilisation d'un nœud de calcul, où un script de nœud de calcul écoute les messages du nœud principal et répond en renvoyant ses propres messages:

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 est disponible dans la plupart des navigateurs depuis plus de 10 ans. Bien que cela qu'ils bénéficient d'une excellente compatibilité avec les navigateurs et d'une bonne optimisation, ce qui signifie aussi qu'ils disposent sont antérieures aux modules JavaScript. Étant donné qu'il n'existait pas de système de modules lors de la conception des nœuds de calcul, l'API permettant de charger du code dans un nœud de calcul et de rédiger des scripts est resté le même que le script synchrone. différentes approches de chargement courantes en 2009.

Historique: nœuds de calcul classiques

Le constructeur de nœud de calcul utilise un modèle classique du script, qui est par rapport à l'URL du document. Elle renvoie immédiatement une référence à la nouvelle instance de nœud de calcul, qui expose une interface de messagerie ainsi qu'une méthode terminate() qui s'arrête immédiatement et et détruit le nœud de calcul.

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

Les nœuds de calcul Web peuvent utiliser une fonction importScripts() pour charger du code supplémentaire, mais elle met en pause l'exécution du worker afin de récupérer et d'évaluer chaque script. Il exécute également des scripts dans le champ d'application global comme une balise <script> classique, ce qui signifie que les variables d'un script peuvent être remplacées par les variables d'un autre.

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

Pour cette raison, les travailleurs Web ont toujours imposé un impact considérable sur l'architecture d'une application. Les développeurs ont dû créer des outils et des solutions ingénieux pour permettre utilisent des nœuds de calcul Web sans renoncer aux pratiques de développement modernes. Par exemple, les bundles Webpack intègre une petite implémentation de chargeur de module dans le code généré qui utilise importScripts(). pour le chargement du code, mais encapsule les modules dans des fonctions pour éviter les conflits de variables et simuler les importations et les exportations de dépendances.

Saisir les nœuds de calcul de module

Un nouveau mode destiné aux travailleurs Web, offrant les avantages de JavaScript en termes d'ergonomie et de performances modules sont disponibles dans Chrome 80, appelés "nœuds de calcul de module". La Le constructeur Worker accepte désormais une nouvelle option {type:"module"}, qui modifie le chargement du script et pour qu'elle corresponde à <script type="module">.

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

Les nœuds de calcul de module étant des modules JavaScript standards, ils peuvent utiliser des instructions d'importation et d'exportation. En tant que avec tous les modules JavaScript, les dépendances ne sont exécutées qu'une seule fois dans un contexte donné (thread principal, worker, etc.), et toutes les importations ultérieures font référence à l'instance de module déjà exécutée. Chargement et l'exécution des modules JavaScript est également optimisée par les navigateurs. Les dépendances d'un module peuvent être avant l'exécution du module, ce qui permet de charger des arborescences de modules entiers en parallèle. Le chargement des modules met également en cache le code analysé, ce qui signifie que les modules utilisés sur le dans un thread et dans un worker n'ont besoin d'être analysés qu'une seule fois.

Le passage aux modules JavaScript permet également d'utiliser des importer pour du code à chargement différé sans bloquer l'exécution de le nœud de calcul. L'importation dynamique est beaucoup plus explicite que l'utilisation de importScripts() pour charger les dépendances. puisque les exportations du module importé sont renvoyées au lieu de dépendre de variables globales.

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

Pour garantir d'excellentes performances, l'ancienne méthode importScripts() n'est pas disponible dans le module les nœuds de calcul. Lorsque les nœuds de calcul utilisent des modules JavaScript, tout le code est chargé de façon stricte mode. Autre le changement notable est que la valeur de this dans le champ d'application de premier niveau d'un module JavaScript est undefined, tandis que dans les nœuds de calcul classiques, la valeur correspond au champ d'application global du nœud de calcul. Heureusement, a toujours été un élément self global qui fournit une référence au champ d'application global. Elle est disponible dans à tous les types de nœuds de calcul, y compris les service workers, mais aussi dans le DOM.

Précharger les nœuds de calcul avec modulepreload

Les nœuds de calcul de modules offrent une amélioration significative des performances : les nœuds de calcul et leurs dépendances. Avec les nœuds de calcul de module, les scripts sont chargés et exécutés de façon standard Les modules JavaScript, ce qui signifie qu'ils peuvent être préchargés et même analysés à l'aide de 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>

Les modules préchargés peuvent également être utilisés à la fois par le thread principal et les nœuds de calcul de module. Ceci est utile pour modules qui sont importés dans les deux contextes ou lorsqu'il n'est pas possible de connaître à l'avance si un module sera utilisé sur le thread principal ou dans un nœud de calcul.

Auparavant, les options de préchargement des scripts de nœud de calcul Web étaient limitées nécessairement fiables. Les nœuds de calcul classiques disposaient de leur propre "nœud de calcul" type de ressource pour le préchargement, navigateurs ont implémenté <link rel="preload" as="worker">. Par conséquent, la technique principale disponible pour le préchargement des workers Web consistait à utiliser <link rel="prefetch">, qui reposait entièrement sur le cache HTTP. Associées aux en-têtes de mise en cache appropriés, elles permettaient pour éviter que l'instanciation des nœuds de calcul n'ait à attendre avant de télécharger leur script. Toutefois, contrairement modulepreload : cette technique ne permettait pas de précharger les dépendances ni de préparer l'analyse.

Qu'en est-il des nœuds de calcul partagés ?

Les nœuds de calcul partagés a été mis à jour pour prendre en charge les modules JavaScript à partir de Chrome 83. Tout comme les travailleurs dédiés, la construction d'un nœud de calcul partagé avec l'option {type:"module"} charge désormais le script du nœud de calcul en tant que au lieu d'un script classique:

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

Avant la prise en charge des modules JavaScript, le constructeur SharedWorker() n'attendait qu'une URL et un argument name facultatif. Elle continuera de fonctionner pour l'utilisation classique des nœuds de calcul partagés. cependant La création de nœuds de calcul partagés pour le module nécessite l'utilisation du nouvel argument options. Les disponibles d'entraînement sont les mêmes que celles d'un nœud de calcul dédié, y compris l'option name qui remplace l'argument name précédent.

Qu'en est-il du service worker ?

La spécification du service worker a déjà été mis à jour pour permettre l'acceptation module JavaScript comme point d'entrée, en utilisant la même option {type:"module"} que les nœuds de calcul de module, Toutefois, cette modification n'a pas encore été implémentée dans les navigateurs. Une fois que cela se produira, il sera possible pour instancier un service worker à l'aide d'un module JavaScript à l'aide du code suivant:

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

Maintenant que la spécification a été mise à jour, les navigateurs commencent à implémenter le nouveau comportement. Cela prend du temps, car l'importation de JavaScript présente des complications supplémentaires. et des modules au service worker. L'enregistrement du service worker doit comparer les scripts importés par la version en cache précédente déterminer s'il faut déclencher une mise à jour, ce qui doit être implémenté pour les modules JavaScript pour les service workers. De plus, les service workers doivent pouvoir contourner cache pour les scripts dans certains cas, recherchant des mises à jour.

Autres ressources et articles complémentaires