Gérer des tâches de longue durée dans une application React avec des Web Workers
À propos de l'auteur
Développeur frontend génial qui aime tout coder. Je suis un amoureux de la musique chorale et je travaille pour la rendre plus accessible au monde, une mise en ligne à un… En savoir plus sur Chidi …
Dans ce didacticiel, nous allons apprendre à utiliser Web Worker API pour gérer les tâches chronophages et bloquant l'interface utilisateur dans une application JavaScript en créant un exemple d'application Web qui exploite les Web Workers. Enfin, nous terminerons l'article en transférant le tout vers une application React.
Le temps de réponse est un gros problème pour les applications Web. Les utilisateurs exigent des réponses instantanées, peu importe ce que fait votre application. Qu'il s'agisse d'afficher uniquement le nom d'une personne ou de calculer les chiffres, les utilisateurs d'applications Web exigent que votre application réponde à leur commande à chaque fois. Parfois, cela peut être difficile à réaliser étant donné la nature mono-thread de JavaScript. Mais dans cet article, nous allons apprendre comment nous pouvons tirer parti de l ' API Web Worker pour offrir une meilleure expérience.
En écrivant cet article, j'ai fait les hypothèses suivantes:
Pour pouvoir suivre, vous devriez avoir au moins une certaine familiarité avec JavaScript et l'API de document ;
Vous devriez également avoir une connaissance pratique de React pour pouvoir démarrer avec succès un nouveau React projet utilisant Create React App .
Si vous avez besoin de plus d'informations sur ce sujet, j'ai inclus un certain nombre de liens dans la section « Autres ressources » pour vous aider à vous lever
Tout d'abord, commençons avec les Web Workers.
Qu'est-ce qu'un Web Worker?
Pour comprendre les Web Workers et le problème qu'ils sont censés résoudre, il est nécessaire de comprendre comment Le code JavaScript est exécuté au moment de l'exécution. Pendant l'exécution, le code JavaScript est exécuté de manière séquentielle et étape par étape. Une fois qu'un morceau de code se termine, le suivant en ligne commence à s'exécuter, et ainsi de suite. En termes techniques, nous disons que JavaScript est mono-thread. Ce comportement implique qu'une fois qu'un morceau de code commence à s'exécuter, chaque code qui suit doit attendre que ce code termine son exécution. Ainsi, chaque ligne de code «bloque» l'exécution de tout ce qui vient après. Il est donc souhaitable que chaque morceau de code se termine le plus rapidement possible. Si un morceau de code prend trop de temps à se terminer, notre programme semble avoir cessé de fonctionner. Sur le navigateur, cela se manifeste par une page figée et qui ne répond pas. Dans certains cas extrêmes, l'onglet se fige complètement.
Imaginez conduire sur une seule voie. Si l'un des conducteurs devant vous arrête de bouger pour une raison quelconque, vous avez un embouteillage. Avec un programme comme Java, le trafic pourrait se poursuivre sur d'autres voies. Ainsi Java est dit multi-thread. Les Web Workers tentent d'intégrer le comportement multithread à JavaScript.
La capture d'écran ci-dessous montre que l'API Web Worker est prise en charge par de nombreux navigateurs, vous devriez donc être sûr de l'utiliser.
Prise en charge du navigateur Web Workers. ( Grand aperçu )
Les Web Workers s'exécutent dans les threads d'arrière-plan sans interférer avec l'interface utilisateur, et ils communiquent avec le code qui les a créés via des gestionnaires d'événements.
Une excellente définition d'un Web Worker vient de MDN :
«Un worker est un objet créé à l'aide d'un constructeur (par exemple Worker () qui exécute un fichier JavaScript nommé – ce fichier contient le code qui s'exécutera dans le thread de travail; les travailleurs s'exécutent dans un autre contexte global différent de la fenêtre actuelle . Ainsi, en utilisant le raccourci window pour obtenir la portée globale actuelle (au lieu de self [19659021] dans un Worker renverra une erreur. ”
Un worker est créé à l'aide du constructeur Worker .
const worker = new Worker ('worker-file.js ')
Il est possible d'exécuter la plupart du code dans un web worker, à quelques exceptions près. Par exemple, vous ne pouvez pas manipuler e le DOM depuis l'intérieur d'un worker. Il n'y a pas d'accès au document API .
Les ouvriers et le thread qui les génère s'envoient des messages en utilisant la méthode postMessage () . De même, ils répondent aux messages en utilisant le gestionnaire d'événements onmessage . Il est important de comprendre cette différence. L'envoi de messages est réalisé à l'aide d'une méthode; la réception d'un message nécessite un gestionnaire d'événements. Le message reçu est contenu dans l'attribut data de l'événement. Nous en verrons un exemple dans la section suivante. Mais permettez-moi de mentionner rapidement que le type de travailleur dont nous avons parlé s’appelle un «travailleur dévoué». Cela signifie que le worker n'est accessible qu'au script qui l'a appelé. Il est également possible d'avoir un worker accessible à partir de plusieurs scripts. Ils sont appelés nœuds de calcul partagés et sont créés à l'aide du constructeur SharedWorker comme illustré ci-dessous.
const sWorker = new SharedWorker ('shared-worker-file.js')
Pour en savoir plus sur les nœuds de calcul , veuillez consulter cet article MDN . Le but de cet article est de vous familiariser avec l'utilisation des agents Web. Allons-y en calculant le nième nombre de Fibonacci.
Calcul du nième nombre de Fibonacci
Remarque: Pour cette section et les deux suivantes, j'utilise Live Server sur VSCode pour exécuter l'application. Vous pouvez certainement utiliser autre chose.
C'est la section que vous attendiez. Nous allons enfin écrire du code pour voir les Web Workers en action. Eh bien, pas si vite. Nous n’apprécierions pas le travail accompli par un Web Worker à moins que nous ne soyons confrontés au genre de problèmes qu’il résout. Dans cette section, nous allons voir un exemple de problème, et dans la section suivante, nous verrons comment un travailleur Web nous aide à faire mieux.
Imaginez que vous construisiez une application Web permettant aux utilisateurs de calculer le nième Numéro de Fibonacci. Si vous êtes nouveau dans le terme «nombre de Fibonacci», vous pouvez en savoir plus ici mais en résumé, les nombres de Fibonacci sont une suite de nombres telle que chaque nombre est la somme des deux précédents
Mathématiquement, il s’exprime comme suit:
Ainsi, les premiers nombres de la séquence sont:
1, 1, 2, 3, 5, 8, 13, 21, 34, 55 , 89 ...
Dans certaines sources, la séquence commence à F 0 = 0 auquel cas la formule ci-dessous est valable pour n > 1 :
Dans cet article, nous commencerons par F 1 = 1. Une chose que nous pouvons voir tout de suite à partir de la formule est que les nombres suivent un modèle récursif. La tâche à accomplir maintenant est d'écrire une fonction récursive pour calculer le nième nombre de Fibonacci (FN).
Après quelques essais, je crois que vous pouvez facilement trouver la fonction ci-dessous.
const fib = n => {
si (n <2) {
retourne n // ou 1
} autre {
retourne fib (n - 1) + fib (n - 2)
}
}
La fonction est simple. Si n est inférieur à 2, renvoie n (ou 1), sinon, renvoie la somme des FN n-1 et n-2 . Avec les fonctions fléchées et l'opérateur ternaire nous pouvons proposer une ligne unique.
Cette fonction a une complexité temporelle de 0 (2 n ) . Cela signifie simplement que lorsque la valeur de n augmente, le temps nécessaire pour calculer la somme augmente de façon exponentielle. Cela en fait une tâche très longue qui pourrait potentiellement interférer avec notre interface utilisateur, pour de grandes valeurs de n. Voyons cela en action.
Note : Ce n’est en aucun cas la meilleure façon de résoudre ce problème particulier. Mon choix d'utiliser cette méthode est pour les besoins de cet article.
Pour commencer, créez un nouveau dossier et nommez-le comme vous voulez. Maintenant, dans ce dossier, créez un dossier src / . Créez également un fichier index.html dans le dossier racine. Dans le dossier src / créez un fichier nommé index.js .
Ouvrez index.html et ajoutez le code HTML suivant.
Calcul du nième nombre de Fibonnaci
Cette partie est très simple. Premièrement, nous avons un titre. Ensuite, nous avons un conteneur avec une entrée et un bouton. Un utilisateur saisit un nombre puis clique sur «Calculer». Nous avons également un conteneur pour contenir le résultat du calcul. Enfin, nous incluons le fichier src / index.js dans une balise script .
Vous pouvez supprimer le lien de la feuille de style. Mais si vous manquez de temps, j'ai défini des CSS que vous pouvez utiliser. Créez simplement le fichier styles.css dans le dossier racine et ajoutez les styles ci-dessous:
body {
marge: 0;
rembourrage: 0;
box-dimensionnement: border-box;
}
.body-conteneur,
.heading-container {
rembourrage: 0 20px;
}
.heading-container {
rembourrage: 20px;
Couleur blanche;
arrière-plan: # 7a84dd;
}
.heading-container> h1 {
marge: 0;
}
.body-container {
largeur: 50%
}
.input-div {
margin-top: 15px;
marge inférieure: 15px;
affichage: flex;
align-items: centre;
}
.résultats {
largeur: 50vw;
}
.résultats> p {
taille de la police: 24px;
}
.result-div {
rembourrage: 5px 10px;
rayon de la bordure: 5px;
marge: 10px 0;
couleur de fond: # e09bb7;
}
.result-div p {
marge: 5px;
}
span.bold {
poids de la police: gras;
}
contribution {
taille de la police: 25px;
}
p.error {
La couleur rouge;
}
.number-input {
rembourrage: 7,5 px 10 px;
}
.btn-submit {
rembourrage: 10px;
rayon de la bordure: 5px;
bordure: aucune;
arrière-plan: # 07f;
taille de la police: 24px;
Couleur blanche;
curseur: pointeur;
marge: 0 10px;
}
const fib = (n) => (n {
// 1er, 2ème, 3ème, 4ème, etc.
const j = num% 10;
const k = num% 100;
commutateur (vrai) {
cas j === 1 && k! == 11:
retourne num + "st";
cas j === 2 && k! == 12:
retourne num + "nd";
cas j === 3 && k! == 13:
retourne num + "rd";
défaut:
retourne num + "th";
}
};
const textCont = (n, fibNum, heure) => {
const nth = suffixe_ordinal (n);
retour '
Heure: $ {heure} ms
$ {nth} numéro de fibonnaci: $ {fibNum}
`;
};
Ici, nous avons trois fonctions. La première est la fonction que nous avons vue précédemment pour calculer le nième FN. La deuxième fonction est simplement une fonction utilitaire pour attacher un suffixe approprié à un nombre entier. La troisième fonction prend quelques arguments et produit un balisage que nous insérerons plus tard dans le DOM. Le premier argument est le nombre dont FN est en cours de calcul. Le deuxième argument est le FN calculé. Le dernier argument est le temps qu'il faut pour effectuer le calcul.
Toujours dans src / index.js ajoutez le code ci-dessous juste sous le précédent.
const errPar = document.getElementById ( "Erreur");
const btn = document.getElementById ("submit-btn");
const input = document.getElementById ("number-input");
const resultsContainer = document.getElementById ("conteneur de résultats");
btn.addEventListener ("clic", (e) => {
errPar.textContent = '';
const num = window.Number (input.value);
si (num <2) {
errPar.textContent = "Veuillez saisir un nombre supérieur à 2";
revenir;
}
const startTime = new Date (). getTime ();
const somme = fib (num);
const time = new Date (). getTime () - startTime;
const resultDiv = document.createElement ("div");
resultDiv.innerHTML = textCont (num, somme, heure);
resultDiv.className = "result-div";
resultsContainer.appendChild (resultDiv);
});
Tout d'abord, nous utilisons l'API document pour obtenir les nœuds DOM dans notre fichier HTML. Nous obtenons une référence au paragraphe où nous afficherons les messages d'erreur; l'entrée; le bouton de calcul et le conteneur dans lequel nous afficherons nos résultats.
Ensuite, nous attachons un gestionnaire d'événements "clic" au bouton. Lorsque le bouton est cliqué, nous prenons tout ce qui se trouve à l'intérieur de l'élément d'entrée et le convertissons en un nombre, si nous obtenons quelque chose de moins de 2, nous affichons un message d'erreur et retournons. Si nous obtenons un nombre supérieur à 2, nous continuons. Tout d'abord, nous enregistrons l'heure actuelle. Après cela, nous calculons le FN. Lorsque cela se termine, nous obtenons un décalage horaire qui représente la durée du calcul. Dans la partie restante du code, nous créons un nouveau div . Nous définissons ensuite son code HTML interne comme la sortie de la fonction textCont () que nous avons définie précédemment. Enfin, nous lui ajoutons une classe (pour le style) et l'ajoutons au conteneur de résultats. L'effet de ceci est que chaque calcul apparaîtra dans un div séparé sous le précédent.
Nous pouvons voir que lorsque le nombre augmente, le temps de calcul augmente également (de façon exponentielle). Par exemple, de 30 à 35, nous avons fait passer le temps de calcul de 13 ms à 130 ms. On peut encore considérer ces opérations comme «rapides». À 40, nous voyons un temps de calcul supérieur à 1 seconde. Sur ma machine, c'est là que je commence à remarquer que la page ne répond plus. À ce stade, je ne peux plus interagir avec la page pendant que le calcul est en cours. Je ne peux pas me concentrer sur l’entrée ou faire quoi que ce soit d’autre.
Rappelez-vous quand nous avons dit que JavaScript était monothread? Eh bien, ce thread a été «bloqué» par ce calcul de longue durée, donc tout le reste doit «attendre» qu'il se termine. Il peut commencer à une valeur inférieure ou supérieure sur votre ordinateur, mais vous êtes obligé d'atteindre ce point. Notez qu'il a fallu près de 10 secondes pour calculer celle de 44. S'il y avait d'autres choses à faire sur votre application Web, eh bien, l'utilisateur doit attendre que Fib (44) se termine avant de pouvoir continuer. Mais si vous avez déployé un travailleur Web pour gérer ce calcul, vos utilisateurs pourraient continuer avec autre chose pendant son exécution.
Voyons maintenant comment les travailleurs Web nous aident à surmonter ce problème.
Un exemple de travailleur Web en action [19659003] Dans cette section, nous allons déléguer le travail de calcul du nième FN à un travailleur Web. Cela aidera à libérer le thread principal et à garder notre interface utilisateur réactive pendant que le calcul est en cours.
Démarrer avec les travailleurs Web est étonnamment simple. Voyons comment. Créez un nouveau fichier src / fib-worker.js . et entrez le code suivant.
const fib = (n) => (n {
const {num} = e.data;
const startTime = new Date (). getTime ();
const fibNum = fib (num);
postMessage ({
fibNum,
time: new Date (). getTime () - startTime,
});
};
Notez que nous avons déplacé la fonction qui calcule le nième nombre de Fibonacci, fib dans ce fichier. Ce fichier sera géré par notre web worker.
Rappel dans la section Qu'est-ce qu'un web worker nous avons mentionné que les web workers et leurs parents communiquent en utilisant le gestionnaire d'événements onmessage et méthode postMessage () . Ici, nous utilisons le gestionnaire d'événements onmessage pour écouter les messages du script parent. Une fois que nous recevons un message, nous déstructurons le numéro de l'attribut data de l'événement. Ensuite, nous obtenons l'heure actuelle et commençons le calcul. Une fois le résultat prêt, nous utilisons la méthode postMessage () pour renvoyer les résultats dans le script parent.
Ouvrez src / index.js apportons quelques modifications.
La première chose à faire est de créer le web worker en utilisant le constructeur Worker . Ensuite, dans l'écouteur d'événements de notre bouton, nous envoyons un numéro au worker en utilisant worker.postMessage ({num}) . Après cela, nous définissons une fonction pour écouter les erreurs dans le worker. Ici, nous renvoyons simplement l'erreur. Vous pouvez certainement faire plus si vous le souhaitez, comme l'afficher dans DOM. Ensuite, nous écoutons les messages du travailleur. Une fois que nous recevons un message, nous déstructurons time et fibNum et continuons le processus de les montrer dans le DOM.
Notez qu'à l'intérieur du web worker, le onmessage est disponible dans la portée du travailleur, donc nous aurions pu l'écrire comme self.onmessage et self.postMessage () . Mais dans le script parent, nous devons les attacher au worker lui-même.
Dans la capture d'écran ci-dessous, vous verrez le fichier de travail Web dans l'onglet sources des outils de développement Chrome. Ce que vous devez noter, c'est que l'interface utilisateur reste réactive quel que soit le numéro que vous entrez. Ce comportement est la magie des travailleurs Web.
Un fichier de travail Web en cours d'exécution. ( Grand aperçu )
Nous avons beaucoup progressé avec notre application Web. Mais il y a autre chose que nous pouvons faire pour l’améliorer. Notre implémentation actuelle utilise un seul worker pour gérer chaque calcul. Si un nouveau message arrive alors que l'un d'eux est en cours d'exécution, l'ancien est remplacé. Pour contourner cela, nous pouvons créer un nouveau travailleur pour chaque appel afin de calculer le FN. Voyons comment procéder dans la section suivante.
Utilisation de plusieurs travailleurs Web
Actuellement, nous traitons chaque demande avec un seul travailleur. Ainsi, une demande entrante remplacera une précédente qui n'est pas encore terminée. Ce que nous voulons maintenant, c'est apporter un petit changement pour générer un nouveau web worker pour chaque requête. Nous tuerons ce travailleur une fois que cela sera fait.
Ouvrez src / index.js et déplacez la ligne qui crée le travailleur Web dans le gestionnaire d'événements de clic du bouton. Le gestionnaire d'événements devrait maintenant ressembler à celui ci-dessous.
btn.addEventListener ("click", (e) => {
errPar.textContent = "";
const num = window.Number (input.value);
si (num err;
worker.onmessage = (e) => {
const {time, fibNum} = e.data;
const resultDiv = document.createElement ("div");
resultDiv.innerHTML = textCont (num, fibNum, heure);
resultDiv.className = "result-div";
resultsContainer.appendChild (resultDiv);
worker.terminate () // cette ligne termine le worker
};
});
Nous avons apporté deux modifications.
Nous avons déplacé cette ligne const worker = new window.Worker ("src / fib-worker.js") dans le gestionnaire d'événements de clic du bouton. [19659008] Nous avons ajouté cette ligne worker.terminate () pour rejeter le worker une fois que nous en avons terminé avec lui.
Donc, à chaque clic du bouton, nous créons un nouveau worker pour gérer le calcul. Ainsi, nous pouvons continuer à modifier l'entrée, et chaque résultat apparaîtra à l'écran une fois le calcul terminé. Dans la capture d'écran ci-dessous, vous pouvez voir que les valeurs de 20 et 30 apparaissent avant celle de 45. Mais j'ai commencé 45 en premier. Une fois que la fonction retourne pour 20 et 30, leurs résultats ont été affichés et le travailleur a été licencié. Lorsque tout est terminé, nous ne devrions pas avoir de travailleurs dans l’onglet sources.
Illustration de plusieurs travailleurs indépendants. ( Grand aperçu )
Nous pourrions terminer cet article ici, mais s'il s'agissait d'une application de réaction, comment y intégrerions-nous les travailleurs Web. C'est l'objet de la section suivante.
Web Workers In React
Pour commencer, créez une nouvelle application de réaction en utilisant CRA. Copiez le fichier fib-worker.js dans le dossier public / de votre application react. Mettre le fichier ici découle du fait que les applications React sont des applications d'une seule page. C’est à peu près la seule chose qui est spécifique à l’utilisation du nœud de calcul dans une application de réaction. Tout ce qui en découle est purement React.
Dans le dossier src / créez un fichier helpers.js et exportez la fonction ordinal_suffix () à partir de celui-ci.
// src / helpers.js
export const ordinal_suffix = (num) => {
// 1er, 2ème, 3ème, 4ème, etc.
const j = num% 10;
const k = num% 100;
commutateur (vrai) {
cas j === 1 && k! == 11:
retourne num + "st";
cas j === 2 && k! == 12:
retourne num + "nd";
cas j === 3 && k! == 13:
retourne num + "rd";
défaut:
retourne num + "th";
}
};
Notre application nous obligera à maintenir un état, alors créez un autre fichier, src / reducer.js et collez-le dans le réducteur d'état.
Passons en revue chaque type d'action l'un après l'autre.
SET_ERROR : définit un état d'erreur lorsqu'il est déclenché.
SET_NUMBER : définit la valeur dans notre
SET_FIBO : ajoute une nouvelle entrée au tableau des FN calculés.
UPDATE_FIBO : ici nous cherchons une entrée particulière et la remplaçons par un nouvel objet qui a le FN calculé et le temps nécessaire pour le calculer.
Nous utiliserons ce réducteur sous peu. Avant cela, créons le composant qui affichera les FN calculés. Créez un nouveau fichier src / Results.js et collez le code ci-dessous.
Avec cette modification, nous commençons le processus de conversion de notre précédent fichier index.html en jsx. Ce fichier a une responsabilité: prendre un tableau d'objets représentant les FN calculés et les afficher. La seule différence par rapport à ce que nous avions auparavant est l'introduction d'un état de chargement . Alors maintenant, lorsque le calcul est en cours d'exécution, nous montrons l'état de chargement pour informer l'utilisateur que quelque chose se passe.
Insérons les derniers éléments en mettant à jour le code dans src / App.js . Le code est assez long, nous allons donc le faire en deux étapes. Ajoutons le premier bloc de code.
Comme d'habitude, nous apportons nos importations. Ensuite, nous instancions une fonction d'état et de mise à jour avec le hook useReducer . Nous définissons ensuite une fonction, runWorker () qui prend un numéro et un ID et se met à appeler un web worker pour calculer le FN pour ce nombre.
Notez que pour créer le worker, nous passons un chemin relatif vers le constructeur de travail. Lors de l'exécution, notre code React est attaché au fichier public / index.html ainsi il peut trouver le fichier fib-worker.js dans le même répertoire. Lorsque le calcul est terminé (déclenché par worker.onmessage ), l'action UPDATE_FIBO est envoyée et le travailleur est arrêté par la suite. Ce que nous avons maintenant n'est pas très différent de ce que nous avions auparavant.
Dans le bloc de retour de ce composant, nous rendons le même HTML que nous avions auparavant. Nous transmettons également le tableau de nombres calculés au composant pour le rendu.
Ajoutons le dernier bloc de code dans l'instruction return .
Nous avons défini un gestionnaire onChange sur l'entrée pour mettre à jour la variable d'état info.num . Sur le bouton, nous définissons un gestionnaire d'événements onClick . Lorsque le bouton est cliqué, nous vérifions si le nombre est supérieur à 2. Notez qu'avant d'appeler runWorker () nous envoyons d'abord une action pour ajouter une entrée au tableau des FN calculés. C'est cette entrée qui sera mise à jour une fois que le travailleur aura terminé son travail. De cette façon, chaque entrée conserve sa position dans la liste, contrairement à ce que nous avions auparavant.
Enfin, copiez le contenu de styles.css d'avant et remplacez le contenu de App.css .
Nous avons maintenant tout en place. Maintenant, démarrez votre serveur de réaction et jouez avec quelques chiffres. Prenez note de l'état de chargement, qui est une amélioration UX. Notez également que l'interface utilisateur reste réactive même lorsque vous entrez un nombre aussi élevé que 1000 et cliquez sur «Calculer».
Affichage de l'état de chargement et du web worker actif. ( Grand aperçu )
Notez l'état de chargement et le travailleur actif. Une fois que la 46ème valeur est calculée, l'ouvrier est tué et l'état de chargement est remplacé par le résultat final.
Conclusion
Ouf! Le trajet a été long, alors terminons-le. Je vous encourage à jeter un œil à l'entrée MDN pour les web workers (voir la liste des ressources ci-dessous) pour découvrir d'autres façons d'utiliser les web workers.
Dans cet article, nous avons appris ce que sont les web workers et le type de problèmes qu'ils rencontrent. re destiné à résoudre. Nous avons également vu comment les implémenter en utilisant du JavaScript brut. Enfin, nous avons vu comment implémenter des web workers dans une application React.
Je vous encourage à profiter de cette excellente API pour offrir une meilleure expérience à vos utilisateurs.