Skip to content

Vous souhaitez recevoir de l'aide sur ce sujet ? rejoignez la communauté Angular.fr sur Discord.

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 ou indexedDB
  • Pas d'accès aux variables globales comme window ou document

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éé :

  1. Un WebWorker qui effectue un traitement intensif sur une liste d'utilisateurs
  2. 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ées
  • onmessage : 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)

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
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 :

  1. Le worker expose une fonction fibonacci via Comlink.expose
  2. Le composant utilise Comlink.wrap pour créer un proxy vers le worker
  3. Les appels de fonction sont asynchrones et retournent des Promises
  4. 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