Appearance
Les WebWorkers dans Angular : Boostez les performances de votre application
Les applications web modernes doivent souvent effectuer des calculs complexes ou traiter de grandes quantités de données. Cependant, JavaScript étant mono-thread, ces opérations peuvent ralentir votre interface utilisateur et créer une mauvaise expérience utilisateur. C'est là que les WebWorkers entrent en jeu !
Qu'est-ce qu'un WebWorker ?
Un WebWorker est un script qui s'exécute en arrière-plan, dans un thread séparé du thread principal de votre application. Cela permet d'effectuer des calculs intensifs sans bloquer l'interface utilisateur.
Avantages :
- Exécution parallèle des tâches lourdes
- Interface utilisateur fluide et réactive
- Meilleure gestion des ressources
- Performance globale améliorée
Quand utiliser un WebWorker ?
Les WebWorkers sont particulièrement utiles pour :
- Les calculs mathématiques complexes
- Le traitement de grandes quantités de données
- Les opérations de cryptographie
- Les analyses en temps réel
- Les transformations d'images
Ne pas utiliser pour des opérations simples car la communication entre threads a un coût !
De plus, les WebWorkers ont les limitations suivantes :
- Pas d'accès au DOM
- Pas d'accès aux API Web comme
localStorage
ouindexedDB
- Pas d'accès aux variables globales comme
window
oudocument
Imaginez que vous êtes dans un restaurant. Le serveur (thread principal) prend votre commande et s'occupe de votre table. S'il devait aussi cuisiner (tâche intensive), il ne pourrait plus s'occuper des autres clients. C'est pourquoi il y a des cuisiniers (WebWorkers) en cuisine qui s'occupent de la préparation des plats pendant que le serveur continue à interagir avec les clients.
Dans ce tutoriel, nous allons voir comment implémenter les WebWorkers dans une application Angular pour traiter une liste d'utilisateurs.
Mise en place du WebWorker
Commençons par créer notre WebWorker. Angular CLI nous facilite la tâche avec une commande dédiée :
Terminal
ng generate web-worker user
vous pouvez mettre un chemin à la place de
user
pour créer le fichier dans un autre dossier.
Cette commande va créeer plusieurs fichiers : tsconfig.worker.json
, user.worker.ts
et ajouter "webWorkerTsConfig": "tsconfig.worker.json"
dans le fichier angular.json
.
Modifions le fichier user.worker.ts
pour ajouter le code du WebWorker :
ts
import { Component } from '@angular/core';
import { User } from './user.interface';
@Component({
selector: 'app-user',
template: `
<div>
@if (isLoading) {
<p>Traitement en cours...</p>
}
@for (user of users; track user.id) {
<div>{{ user.name }} ({{ user.email }})</div>
}
</div>
`
})
export class UserComponent {
// Liste des utilisateurs à traiter
users: User[] = [];
isLoading = false;
// On crée une instance du WebWorker
private worker: Worker;
constructor() {
// Initialisation du WebWorker
if (typeof Worker !== 'undefined') {
this.worker = new Worker(new URL('./workers/user.worker', import.meta.url));
// On écoute les messages du WebWorker
this.worker.onmessage = ({ data }) => {
this.users = data;
this.isLoading = false;
};
}
}
// Méthode pour lancer le traitement dans le WebWorker
processUsers(users: User[]) {
this.isLoading = true;
this.worker.postMessage(users);
}
}
ts
/// <reference lib="webworker" />
// Le WebWorker écoute les messages du thread principal
addEventListener('message', ({ data }) => {
// On récupère la liste des utilisateurs
const users: User[] = data;
// On effectue un traitement intensif (exemple : filtrage complexe)
const processedUsers = users.filter(user => {
// Simulation d'un traitement lourd
let result = false;
for(let i = 0; i < 1000000; i++) {
result = user.email.includes('@') && user.name.length > 3;
}
return result;
});
// On renvoie le résultat au thread principal
postMessage(processedUsers);
});
Dans cet exemple, nous avons créé :
- Un WebWorker qui effectue un traitement intensif sur une liste d'utilisateurs
- Un composant qui utilise ce WebWorker
Compatibilité
Vérifiez toujours si le navigateur supporte les WebWorkers avant de les utiliser :
ts
if (typeof Worker !== 'undefined') {
// Création du WebWorker
}
Ensuite, nous avons créer une instance du WebWorker dans le composant. Nous mettons en paramètre le chemin vers le fichier user.worker.ts
import.meta.url
import.meta.url
est une propriété de l'objet import.meta
qui renvoie l'URL du module en cours d'exécution.
Dans le cas d'un WebWorker, il renvoie l'URL du fichier qui contient le code du worker, et non pas l'URL de la page actuelle.
La communication se fait via le système de messages :
postMessage()
: pour envoyer des donnéesonmessage
: pour recevoir des données
Bonnes pratiques
- Utilisez les WebWorkers uniquement pour des tâches intensives
- Ne partagez pas de variables entre le thread principal et le WebWorker
- Évitez de créer trop de WebWorkers (ils consomment des ressources)
Utilisation de Comlink
Comlink est une bibliothèque qui simplifie considérablement la communication avec les WebWorkers en permettant d'appeler des fonctions du worker comme si elles étaient locales.
Avantages de Comlink
Comlink nous permet de :
- Appeler des fonctions du worker de manière asynchrone avec
await
- Éviter la gestion manuelle des événements
postMessage
- Avoir une API plus intuitive et proche du code synchrone
Installation de Comlink
Terminal
npm install comlink
ng generate web-worker fib-worker
Exemple avec le calcul de Fibonacci
Créons un exemple concret avec le calcul de la suite de Fibonacci, une opération qui peut devenir très intensive pour de grands nombres.
Fibonacci est une suite de nombres où chaque nombre est la somme des deux précédents : 0, 1, 1, 2, 3, 5, 8, 13, 21, etc.
ts
import { Component, afterNextRender } from '@angular/core';
import * as Comlink from 'comlink';
interface FibWorker {
fibonacci(n: number): Promise<number>;
}
@Component({
selector: 'app-fibonacci',
template: `
<div class="fibonacci-calculator">
<input type="number" [(ngModel)]="n" placeholder="Entrez un nombre">
<button (click)="calculateFibonacci()" [disabled]="isCalculating">
Calculer Fibonacci
</button>
@if (isCalculating) {
<p>Calcul en cours...</p>
}
@if (result !== null) {
<div class="result">
Fibonacci({{n}}) = {{ result }}
</div>
}
</div>
`,
styles: `
.fibonacci-calculator {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.result {
margin-top: 20px;
font-weight: bold;
}
`
})
export class FibonacciComponent {
// Type correct pour le proxy
private workerProxy: FibWorker | null = null;
n: number = 0;
result: number | null = null;
isCalculating = false;
constructor() {
// On initialise le worker après le rendu pour éviter les problèmes de SSR
afterNextRender(() => {
if (typeof Worker !== 'undefined') {
const worker = new Worker(
new URL('./workers/fib.worker', import.meta.url),
{ type: 'module' }
);
this.workerProxy = Comlink.wrap<FibWorker>(worker);
}
});
}
async calculateFibonacci() {
if (!this.workerProxy) return;
this.isCalculating = true;
try {
this.result = await this.workerProxy.fibonacci(this.n);
} catch (error) {
console.error('Erreur lors du calcul:', error);
} finally {
this.isCalculating = false;
}
}
}
ts
/// <reference lib="webworker" />
import * as Comlink from 'comlink';
// Fonction de calcul de Fibonacci (itérative pour de meilleures performances)
function fibonacci(n: number): number {
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
const temp = a + b;
a = b;
b = temp;
}
return b;
}
// On expose la fonction pour qu'elle soit accessible via Comlink
Comlink.expose({ fibonacci });
Dans cet exemple :
- Le worker expose une fonction
fibonacci
viaComlink.expose
- Le composant utilise
Comlink.wrap
pour créer un proxy vers le worker - Les appels de fonction sont asynchrones et retournent des Promises
- L'interface utilisateur reste réactive même pendant les calculs intensifs
Performance
Pour les très grands nombres de Fibonacci, même un worker peut prendre du temps. Il est important de :
- Toujours montrer un indicateur de chargement
- Gérer les erreurs correctement
- Limiter les valeurs d'entrée si nécessaire