Cours JavaScript et TypeScript · Module 10: TypeScript en pratique
Web Workers pour les calculs lourds
Dans cette leçon7
Imagine que le laboratoire du parc séquence l'ADN du T-Rex et que, pendant ce temps, la salle de contrôle se fige : les caméras sont bloquées et le bouton d'alarme ne répond plus. C'est ainsi que se comporte le navigateur quand des calculs lourds atterrissent dans le thread principal. Les Web Workers sont une technologie qui permet d'exécuter du JavaScript dans des threads séparés, indépendamment du thread principal de l'UI (User Interface), ce qui garde l'interface réactive.
Ce que tu vas apprendre :
- déplacer des calculs vers un thread séparé et dialoguer avec lui via
postMessage - reconnaître les limites d'un worker et distinguer Dedicated, Shared et Service Worker
- construire un pool de workers et partager de la mémoire grâce à
SharedArrayBuffer - mesurer les performances et gérer les pannes des workers
Pourquoi les Web Workers sont-ils nécessaires ?
Le problème de la nature monothread de JavaScript
Ton code s'exécute dans un seul thread principal, qui gère aussi les clics, le défilement et le dessin de la page. Regarde ce qu'une longue boucle lui fait subir :
1// Problème : des calculs bloquants dans le thread principal
2function heavyCalculation(n) {
3 console.log('Début des calculs lourds...');
4
5 // Simulation de calculs intensifs
6 let result = 0;
7 for (let i = 0; i < n; i++) {
8 for (let j = 0; j < 1000000; j++) {
9 result += Math.sqrt(i * j);
10 }
11 }
12
13 console.log('Calculs terminés');
14 return result;
15}
16
17// Cela va bloquer l'UI pendant plusieurs secondes !
18document.getElementById('calculate-button').addEventListener('click', () => {
19 const result = heavyCalculation(5000);
20 document.getElementById('result').textContent = result;
21});
22
23// Pendant les calculs, l'utilisateur ne peut pas :
24// - Cliquer sur d'autres boutons
25// - Faire défiler la page
26// - Profiter des animations
27// - Saisir du texte dans les formulairesTant que les calculs durent, la boucle d'événements (event loop) ne traite ni clic ni image d'animation. C'est précisément ce problème que résolvent les Web Workers.
Comparaison des performances
Un worker ne calcule pas plus vite, car c'est le même moteur JavaScript. La différence tient à qui attend. Cette ébauche de benchmark compare les deux approches :
1// Benchmark: Main Thread vs Web Worker
2const performanceTest = {
3 // Test sans Web Worker - bloque l'UI
4 async testMainThread() {
5 console.time('Main Thread Calculation');
6
7 const start = performance.now();
8 const result = this.calculatePrimes(100000);
9 const end = performance.now();
10
11 console.timeEnd('Main Thread Calculation');
12
13 return {
14 result: result.length,
15 time: end - start,
16 blocked: true
17 };
18 },
19
20 // Test avec Web Worker - ne bloque pas l'UI
21 async testWebWorker() {
22 console.time('Web Worker Calculation');
23
24 const start = performance.now();
25 const result = await this.calculatePrimesWorker(100000);
26 const end = performance.now();
27
28 console.timeEnd('Web Worker Calculation');
29
30 return {
31 result: result.length,
32 time: end - start,
33 blocked: false
34 };
35 },
36
37 calculatePrimes(max) {
38 const primes = [];
39 for (let i = 2; i <= max; i++) {
40 if (this.isPrime(i)) {
41 primes.push(i);
42 }
43 }
44 return primes;
45 },
46
47 isPrime(num) {
48 for (let i = 2; i <= Math.sqrt(num); i++) {
49 if (num % i === 0) return false;
50 }
51 return true;
52 }
53};Le temps est parfois similaire, mais avec un worker, l'interface continue de réagir du début à la fin.
Implémentations de base des Web Workers
Créer un Web Worker simple
Tu crées un worker avec le constructeur new Worker('worker.js'), qui lance le script dans un thread séparé. L'ordre compte : installe d'abord l'écoute onmessage, envoie ensuite les données avec postMessage, et termine enfin le travail avec la méthode terminate() :
1// main.js - le thread principal
2class WebWorkerManager {
3 constructor() {
4 this.worker = null;
5 this.setupWorker();
6 }
7
8 setupWorker() {
9 // Vérification de la prise en charge des Web Workers
10 if (typeof Worker !== 'undefined') {
11 this.worker = new Worker('worker.js');
12
13 // Gestion des messages venant du worker
14 this.worker.onmessage = (event) => {
15 this.handleWorkerMessage(event.data);
16 };
17
18 // Gestion des erreurs
19 this.worker.onerror = (error) => {
20 console.error('Worker error:', error);
21 };
22
23 console.log('Le Web Worker a été créé');
24 } else {
25 console.warn('Les Web Workers ne sont pas pris en charge par ce navigateur');
26 }
27 }
28
29 // Envoi d'une tâche au worker
30 sendTask(taskType, data) {
31 if (this.worker) {
32 this.worker.postMessage({
33 type: taskType,
34 data: data,
35 timestamp: Date.now()
36 });
37 }
38 }
39
40 // Gestion des réponses du worker
41 handleWorkerMessage(message) {
42 switch (message.type) {
43 case 'CALCULATION_COMPLETE':
44 this.displayResult(message.result);
45 break;
46 case 'PROGRESS_UPDATE':
47 this.updateProgress(message.progress);
48 break;
49 case 'ERROR':
50 this.handleError(message.error);
51 break;
52 }
53 }
54
55 // Fin du travail du worker
56 terminateWorker() {
57 if (this.worker) {
58 this.worker.terminate();
59 this.worker = null;
60 console.log('Le Web Worker a été arrêté');
61 }
62 }
63}
64
65// Utilisation
66const workerManager = new WebWorkerManager();
67
68// Envoi d'une tâche de calcul
69document.getElementById('start-calculation').addEventListener('click', () => {
70 workerManager.sendTask('CALCULATE_PRIMES', { max: 100000 });
71});Tu lis la réponse dans event.data, en affectant une fonction à onmessage ou avec addEventListener('message', ...). Les données ne sont pas partagées : le navigateur les copie avec l'algorithme structured clone, donc tu ne peux envoyer ni fonctions ni éléments du DOM.
Implémentation de worker.js
De l'autre côté, le worker écoute via self.onmessage, répond via self.postMessage et peut signaler sa progression :
1// worker.js - le code exécuté dans un thread séparé
2class WorkerCalculations {
3 constructor() {
4 this.setupMessageHandler();
5 }
6
7 setupMessageHandler() {
8 // Handler principal des messages venant du thread principal
9 self.onmessage = (event) => {
10 const { type, data, timestamp } = event.data;
11
12 try {
13 this.handleTask(type, data, timestamp);
14 } catch (error) {
15 this.sendError(error.message);
16 }
17 };
18 }
19
20 handleTask(type, data, timestamp) {
21 switch (type) {
22 case 'CALCULATE_PRIMES':
23 this.calculatePrimes(data.max);
24 break;
25 case 'PROCESS_IMAGE':
26 this.processImage(data.imageData);
27 break;
28 case 'SORT_LARGE_ARRAY':
29 this.sortArray(data.array);
30 break;
31 case 'MONTE_CARLO_PI':
32 this.calculatePiMonteCarlo(data.iterations);
33 break;
34 default:
35 this.sendError(`Unknown task type: ${type}`);
36 }
37 }
38
39 // Calcul des nombres premiers avec suivi de la progression
40 calculatePrimes(max) {
41 const primes = [];
42 const updateInterval = Math.floor(max / 100); // Mise à jour tous les 1 %
43
44 for (let i = 2; i <= max; i++) {
45 if (this.isPrime(i)) {
46 primes.push(i);
47 }
48
49 // Signalement de la progression
50 if (i % updateInterval === 0) {
51 const progress = (i / max) * 100;
52 this.sendProgress(progress);
53 }
54 }
55
56 this.sendResult('CALCULATION_COMPLETE', {
57 primes: primes,
58 count: primes.length,
59 executionTime: performance.now()
60 });
61 }
62
63 isPrime(num) {
64 if (num < 2) return false;
65 for (let i = 2; i <= Math.sqrt(num); i++) {
66 if (num % i === 0) return false;
67 }
68 return true;
69 }
70
71 // Traitement d'image (filtrage, transformations)
72 processImage(imageData) {
73 const { data, width, height, filterType } = imageData;
74
75 switch (filterType) {
76 case 'grayscale':
77 this.applyGrayscaleFilter(data);
78 break;
79 case 'blur':
80 this.applyBlurFilter(data, width, height);
81 break;
82 case 'edge':
83 this.applyEdgeDetection(data, width, height);
84 break;
85 }
86
87 this.sendResult('IMAGE_PROCESSED', {
88 processedData: data,
89 filterApplied: filterType
90 });
91 }
92
93 applyGrayscaleFilter(data) {
94 for (let i = 0; i < data.length; i += 4) {
95 const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
96 data[i] = gray; // Red
97 data[i + 1] = gray; // Green
98 data[i + 2] = gray; // Blue
99 // Alpha (i + 3) remains unchanged
100 }
101 }
102
103 // Tri de grands tableaux
104 sortArray(array) {
105 const startTime = performance.now();
106
107 // Implémentation du merge sort pour de meilleures performances
108 const sortedArray = this.mergeSort(array);
109
110 const endTime = performance.now();
111
112 this.sendResult('SORT_COMPLETE', {
113 sortedArray: sortedArray,
114 originalLength: array.length,
115 executionTime: endTime - startTime
116 });
117 }
118
119 mergeSort(arr) {
120 if (arr.length <= 1) return arr;
121
122 const mid = Math.floor(arr.length / 2);
123 const left = this.mergeSort(arr.slice(0, mid));
124 const right = this.mergeSort(arr.slice(mid));
125
126 return this.merge(left, right);
127 }
128
129 merge(left, right) {
130 const result = [];
131 let leftIndex = 0;
132 let rightIndex = 0;
133
134 while (leftIndex < left.length && rightIndex < right.length) {
135 if (left[leftIndex] <= right[rightIndex]) {
136 result.push(left[leftIndex]);
137 leftIndex++;
138 } else {
139 result.push(right[rightIndex]);
140 rightIndex++;
141 }
142 }
143
144 return result.concat(left.slice(leftIndex), right.slice(rightIndex));
145 }
146
147 // Calcul de Pi par la méthode de Monte-Carlo
148 calculatePiMonteCarlo(iterations) {
149 let pointsInCircle = 0;
150 const updateInterval = Math.floor(iterations / 100);
151
152 for (let i = 0; i < iterations; i++) {
153 const x = Math.random() * 2 - 1; // de -1 à 1
154 const y = Math.random() * 2 - 1; // de -1 à 1
155
156 if (x * x + y * y <= 1) {
157 pointsInCircle++;
158 }
159
160 if (i % updateInterval === 0) {
161 const currentPi = (pointsInCircle / i) * 4;
162 const progress = (i / iterations) * 100;
163
164 this.sendProgress(progress, { currentPi, iteration: i });
165 }
166 }
167
168 const finalPi = (pointsInCircle / iterations) * 4;
169
170 this.sendResult('PI_CALCULATION_COMPLETE', {
171 pi: finalPi,
172 iterations: iterations,
173 accuracy: Math.abs(Math.PI - finalPi)
174 });
175 }
176
177 // Méthodes auxiliaires pour la communication
178 sendResult(type, result) {
179 self.postMessage({
180 type: type,
181 result: result,
182 timestamp: Date.now()
183 });
184 }
185
186 sendProgress(progress, additionalData = {}) {
187 self.postMessage({
188 type: 'PROGRESS_UPDATE',
189 progress: progress,
190 ...additionalData,
191 timestamp: Date.now()
192 });
193 }
194
195 sendError(errorMessage) {
196 self.postMessage({
197 type: 'ERROR',
198 error: errorMessage,
199 timestamp: Date.now()
200 });
201 }
202}
203
204// Initialisation du worker
205new WorkerCalculations();Le worker a sa propre portée globale (self) et n'a accès ni au DOM, ni à document, ni à window. En revanche, il peut utiliser fetch, XMLHttpRequest, setTimeout, setInterval et IndexedDB. Le cycle de vie se présente donc ainsi : création, postMessage(), travail dans le thread du worker et réception du résultat dans onmessage.
Patterns avancés des Web Workers
Worker Pool pour les calculs parallèles
Un pool crée plusieurs workers, généralement autant que de cœurs indiqués par navigator.hardwareConcurrency, et leur répartit les tâches d'une file d'attente :
1// WorkerPool.js - gestion de plusieurs workers
2class WorkerPool {
3 constructor(workerScript, poolSize = navigator.hardwareConcurrency || 4) {
4 this.workerScript = workerScript;
5 this.poolSize = poolSize;
6 this.workers = [];
7 this.taskQueue = [];
8 this.activeTasksCount = 0;
9
10 this.initializePool();
11 }
12
13 initializePool() {
14 for (let i = 0; i < this.poolSize; i++) {
15 const worker = {
16 instance: new Worker(this.workerScript),
17 busy: false,
18 id: i
19 };
20
21 worker.instance.onmessage = (event) => {
22 this.handleWorkerMessage(worker, event.data);
23 };
24
25 worker.instance.onerror = (error) => {
26 this.handleWorkerError(worker, error);
27 };
28
29 this.workers.push(worker);
30 }
31
32 console.log(`Worker pool initialized with ${this.poolSize} workers`);
33 }
34
35 // Exécution d'une tâche à l'aide du pool
36 execute(taskData) {
37 return new Promise((resolve, reject) => {
38 const task = {
39 data: taskData,
40 resolve: resolve,
41 reject: reject,
42 id: this.generateTaskId()
43 };
44
45 const availableWorker = this.getAvailableWorker();
46
47 if (availableWorker) {
48 this.assignTaskToWorker(availableWorker, task);
49 } else {
50 this.taskQueue.push(task);
51 }
52 });
53 }
54
55 getAvailableWorker() {
56 return this.workers.find(worker => !worker.busy);
57 }
58
59 assignTaskToWorker(worker, task) {
60 worker.busy = true;
61 worker.currentTask = task;
62 this.activeTasksCount++;
63
64 worker.instance.postMessage({
65 taskId: task.id,
66 ...task.data
67 });
68 }
69
70 handleWorkerMessage(worker, message) {
71 const task = worker.currentTask;
72
73 if (message.type === 'TASK_COMPLETE') {
74 task.resolve(message.result);
75 this.releaseWorker(worker);
76 } else if (message.type === 'TASK_ERROR') {
77 task.reject(new Error(message.error));
78 this.releaseWorker(worker);
79 } else if (message.type === 'PROGRESS_UPDATE') {
80 // Transmission de la mise à jour de progression (optionnel)
81 if (task.onProgress) {
82 task.onProgress(message.progress);
83 }
84 }
85 }
86
87 releaseWorker(worker) {
88 worker.busy = false;
89 worker.currentTask = null;
90 this.activeTasksCount--;
91
92 // Vérifier s'il reste des tâches dans la file
93 if (this.taskQueue.length > 0) {
94 const nextTask = this.taskQueue.shift();
95 this.assignTaskToWorker(worker, nextTask);
96 }
97 }
98
99 handleWorkerError(worker, error) {
100 console.error(`Worker ${worker.id} error:`, error);
101
102 if (worker.currentTask) {
103 worker.currentTask.reject(error);
104 this.releaseWorker(worker);
105 }
106 }
107
108 // Traitement parallèle de grands ensembles de données
109 async processInParallel(dataArray, chunkProcessor) {
110 const chunkSize = Math.ceil(dataArray.length / this.poolSize);
111 const chunks = [];
112
113 // Découpage des données en chunks
114 for (let i = 0; i < dataArray.length; i += chunkSize) {
115 chunks.push(dataArray.slice(i, i + chunkSize));
116 }
117
118 // Traitement parallèle des chunks
119 const promises = chunks.map(chunk =>
120 this.execute({
121 type: 'PROCESS_CHUNK',
122 chunk: chunk,
123 processor: chunkProcessor.toString()
124 })
125 );
126
127 const results = await Promise.all(promises);
128
129 // Fusion des résultats
130 return results.flat();
131 }
132
133 generateTaskId() {
134 return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
135 }
136
137 // Arrêt de tous les workers
138 terminate() {
139 this.workers.forEach(worker => {
140 worker.instance.terminate();
141 });
142
143 this.workers = [];
144 this.taskQueue = [];
145 console.log('Worker pool terminated');
146 }
147
148 // Statistiques du pool
149 getStats() {
150 return {
151 poolSize: this.poolSize,
152 activeWorkers: this.workers.filter(w => w.busy).length,
153 queueLength: this.taskQueue.length,
154 activeTasks: this.activeTasksCount
155 };
156 }
157}
158
159// Utilisation du Worker Pool
160const workerPool = new WorkerPool('calculation-worker.js', 8);
161
162// Exemple de traitement parallèle
163async function processLargeDataset() {
164 const largeArray = Array.from({ length: 1000000 }, (_, i) => Math.random() * 1000);
165
166 try {
167 const results = await workerPool.processInParallel(largeArray, (chunk) => {
168 // La fonction de traitement sera exécutée dans un worker
169 return chunk.map(x => Math.sqrt(x * x + 1));
170 });
171
172 console.log('Traitement terminé :', results.length);
173 console.log('Stats:', workerPool.getStats());
174 } catch (error) {
175 console.error('Erreur de traitement :', error);
176 }
177}Quand tous les workers sont occupés, la tâche attend dans la file, et releaseWorker transmet aussitôt la suivante. chunkProcessor arrive dans le worker sous forme de texte, car postMessage ne transporte pas les fonctions.
Shared Array Buffer pour une communication efficace
SharedArrayBuffer est une mémoire visible par plusieurs threads à la fois, donc les workers écrivent les résultats de la multiplication de matrices directement dans le tampon commun, sans copie :
1// SharedArrayBufferWorker.js - mémoire partagée entre les workers
2class SharedArrayBufferManager {
3 constructor() {
4 this.checkSupport();
5 }
6
7 checkSupport() {
8 if (typeof SharedArrayBuffer === 'undefined') {
9 console.warn("SharedArrayBuffer n'est pas pris en charge");
10 this.fallbackToTransferableObjects();
11 return false;
12 }
13 return true;
14 }
15
16 // Création d'une mémoire partagée pour les calculs de matrices
17 createSharedMatrixBuffer(rows, cols) {
18 const bufferSize = rows * cols * Float64Array.BYTES_PER_ELEMENT;
19 const sharedBuffer = new SharedArrayBuffer(bufferSize);
20 const matrix = new Float64Array(sharedBuffer);
21
22 return {
23 buffer: sharedBuffer,
24 matrix: matrix,
25 rows: rows,
26 cols: cols,
27
28 // Méthodes auxiliaires (elles utilisent la variable matrix, car dans une fonction fléchée, this désigne le gestionnaire)
29 get: (row, col) => {
30 return matrix[row * cols + col];
31 },
32
33 set: (row, col, value) => {
34 matrix[row * cols + col] = value;
35 },
36
37 fill: (value) => {
38 matrix.fill(value);
39 }
40 };
41 }
42
43 // Multiplication de matrices en parallèle
44 async parallelMatrixMultiply(matrixA, matrixB, workerCount = 4) {
45 const rowsA = matrixA.rows;
46 const colsA = matrixA.cols;
47 const colsB = matrixB.cols;
48
49 // Création de la matrice de résultat partagée
50 const resultMatrix = this.createSharedMatrixBuffer(rowsA, colsB);
51
52 // Répartition du travail entre les workers
53 const rowsPerWorker = Math.ceil(rowsA / workerCount);
54 const workers = [];
55
56 for (let i = 0; i < workerCount; i++) {
57 const startRow = i * rowsPerWorker;
58 const endRow = Math.min(startRow + rowsPerWorker, rowsA);
59
60 if (startRow < rowsA) {
61 const worker = new Worker('matrix-worker.js');
62
63 const promise = new Promise((resolve, reject) => {
64 worker.onmessage = (event) => {
65 if (event.data.type === 'MATRIX_COMPLETE') {
66 worker.terminate();
67 resolve(event.data.result);
68 }
69 };
70
71 worker.onerror = reject;
72 });
73
74 worker.postMessage({
75 type: 'MULTIPLY_MATRIX_ROWS',
76 matrixABuffer: matrixA.buffer,
77 matrixBBuffer: matrixB.buffer,
78 resultBuffer: resultMatrix.buffer,
79 dimensions: {
80 rowsA: rowsA,
81 colsA: colsA,
82 colsB: colsB
83 },
84 rowRange: { start: startRow, end: endRow }
85 });
86
87 workers.push(promise);
88 }
89 }
90
91 await Promise.all(workers);
92 return resultMatrix;
93 }
94
95 fallbackToTransferableObjects() {
96 console.log('Utilisation des Transferable Objects comme alternative');
97 // Implémentation avec des Transferable Objects
98 }
99}
100
101// matrix-worker.js - worker pour les calculs de matrices
102self.onmessage = function(event) {
103 const { type, matrixABuffer, matrixBBuffer, resultBuffer, dimensions, rowRange } = event.data;
104
105 if (type === 'MULTIPLY_MATRIX_ROWS') {
106 const matrixA = new Float64Array(matrixABuffer);
107 const matrixB = new Float64Array(matrixBBuffer);
108 const result = new Float64Array(resultBuffer);
109
110 const { rowsA, colsA, colsB } = dimensions;
111 const { start, end } = rowRange;
112
113 // Multiplication de matrices pour les lignes attribuées
114 for (let i = start; i < end; i++) {
115 for (let j = 0; j < colsB; j++) {
116 let sum = 0;
117 for (let k = 0; k < colsA; k++) {
118 sum += matrixA[i * colsA + k] * matrixB[k * colsB + j];
119 }
120 result[i * colsB + j] = sum;
121 }
122 }
123
124 self.postMessage({
125 type: 'MATRIX_COMPLETE',
126 result: `Rows ${start}-${end} completed`
127 });
128 }
129};Chaque worker calcule des lignes différentes, ils ne se marchent donc pas sur les pieds. Quand des threads modifient les mêmes cellules, utilise Atomics (par exemple Atomics.add et Atomics.wait), dont les opérations sont indivisibles. Les navigateurs ne donnent accès à SharedArrayBuffer qu'aux pages isolées par les en-têtes Cross-Origin-Opener-Policy: same-origin et Cross-Origin-Embedder-Policy: require-corp.
Service Worker pour le Background Processing
En plus du worker ordinaire (dedicated), il en existe deux autres types. Le Shared Worker, créé avec new SharedWorker(), est partagé par les onglets et les fenêtres de la même origine (origin). Le Service Worker se place entre la page et le réseau, et gère le cache, le mode hors ligne ainsi que la synchronisation en arrière-plan :
1// ServiceWorkerManager.js - gestion des tâches longues en arrière-plan
2class ServiceWorkerManager {
3 constructor() {
4 this.serviceWorker = null;
5 this.messageChannel = null;
6 this.setupServiceWorker();
7 }
8
9 async setupServiceWorker() {
10 if ('serviceWorker' in navigator) {
11 try {
12 const registration = await navigator.serviceWorker.register('background-worker.js');
13
14 // Attente de l'activation du service worker
15 await navigator.serviceWorker.ready;
16
17 this.serviceWorker = registration.active || registration.waiting || registration.installing;
18 this.setupMessageChannel();
19
20 console.log('Service Worker enregistré et prêt');
21 } catch (error) {
22 console.error("Erreur d'enregistrement du Service Worker :", error);
23 }
24 }
25 }
26
27 setupMessageChannel() {
28 this.messageChannel = new MessageChannel();
29
30 // Gestion des messages venant du Service Worker
31 this.messageChannel.port1.onmessage = (event) => {
32 this.handleServiceWorkerMessage(event.data);
33 };
34
35 // Envoi du port au Service Worker
36 navigator.serviceWorker.controller?.postMessage(
37 { type: 'INIT_PORT' },
38 [this.messageChannel.port2]
39 );
40 }
41
42 // Confier une tâche longue au Service Worker
43 scheduleBackgroundTask(taskConfig) {
44 if (this.messageChannel) {
45 this.messageChannel.port1.postMessage({
46 type: 'SCHEDULE_TASK',
47 task: {
48 id: this.generateTaskId(),
49 ...taskConfig,
50 scheduledAt: Date.now()
51 }
52 });
53 }
54 }
55
56 // Exemple : synchronisation périodique des données
57 scheduleDataSync(interval = 300000) { // 5 minutes
58 this.scheduleBackgroundTask({
59 type: 'DATA_SYNC',
60 interval: interval,
61 endpoint: '/api/sync',
62 recurring: true
63 });
64 }
65
66 // Exemple : traitement de fichiers en arrière-plan
67 scheduleFileProcessing(files) {
68 files.forEach(file => {
69 this.scheduleBackgroundTask({
70 type: 'FILE_PROCESSING',
71 fileName: file.name,
72 fileData: file,
73 processor: 'image-compression'
74 });
75 });
76 }
77
78 handleServiceWorkerMessage(message) {
79 switch (message.type) {
80 case 'TASK_COMPLETED':
81 this.onTaskCompleted(message.task, message.result);
82 break;
83 case 'TASK_FAILED':
84 this.onTaskFailed(message.task, message.error);
85 break;
86 case 'SYNC_COMPLETED':
87 this.onSyncCompleted(message.result);
88 break;
89 }
90 }
91
92 onTaskCompleted(task, result) {
93 console.log(`Tâche ${task.id} terminée :`, result);
94
95 // Mise à jour de l'UI ou de l'état de l'application
96 this.updateApplicationState(task, result);
97 }
98
99 generateTaskId() {
100 return `bg_task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
101 }
102}
103
104// background-worker.js - Service Worker
105class BackgroundTaskProcessor {
106 constructor() {
107 this.activeTasks = new Map();
108 this.messagePort = null;
109 this.setupEventListeners();
110 }
111
112 setupEventListeners() {
113 self.addEventListener('message', (event) => {
114 if (event.data.type === 'INIT_PORT') {
115 this.messagePort = event.ports[0];
116 this.setupPortListener();
117 }
118 });
119
120 // Background Sync
121 self.addEventListener('sync', (event) => {
122 if (event.tag === 'background-data-sync') {
123 event.waitUntil(this.performDataSync());
124 }
125 });
126 }
127
128 setupPortListener() {
129 this.messagePort.onmessage = (event) => {
130 this.handleTask(event.data);
131 };
132 }
133
134 async handleTask(message) {
135 if (message.type === 'SCHEDULE_TASK') {
136 const task = message.task;
137
138 try {
139 const result = await this.executeTask(task);
140
141 this.messagePort.postMessage({
142 type: 'TASK_COMPLETED',
143 task: task,
144 result: result
145 });
146 } catch (error) {
147 this.messagePort.postMessage({
148 type: 'TASK_FAILED',
149 task: task,
150 error: error.message
151 });
152 }
153 }
154 }
155
156 async executeTask(task) {
157 switch (task.type) {
158 case 'DATA_SYNC':
159 return await this.performDataSync(task);
160 case 'FILE_PROCESSING':
161 return await this.processFile(task);
162 case 'BATCH_CALCULATION':
163 return await this.performBatchCalculation(task);
164 default:
165 throw new Error(`Unknown task type: ${task.type}`);
166 }
167 }
168
169 async performDataSync(task) {
170 try {
171 const response = await fetch(task.endpoint, {
172 method: 'POST',
173 headers: { 'Content-Type': 'application/json' },
174 body: JSON.stringify({ timestamp: Date.now() })
175 });
176
177 if (!response.ok) {
178 throw new Error(`Sync failed: ${response.status}`);
179 }
180
181 const data = await response.json();
182
183 // Enregistrement dans IndexedDB ou dans la Cache API
184 await this.storeDataLocally(data);
185
186 return { success: true, syncedRecords: data.length };
187 } catch (error) {
188 throw new Error(`Data sync failed: ${error.message}`);
189 }
190 }
191
192 async processFile(task) {
193 // Simulation du traitement d'un fichier
194 const { fileName, processor } = task;
195
196 switch (processor) {
197 case 'image-compression':
198 return await this.compressImage(task.fileData);
199 case 'video-transcoding':
200 return await this.transcodeVideo(task.fileData);
201 default:
202 throw new Error(`Unknown processor: ${processor}`);
203 }
204 }
205
206 async storeDataLocally(data) {
207 // Implémentation de l'enregistrement dans IndexedDB
208 return new Promise((resolve, reject) => {
209 const request = indexedDB.open('AppDatabase', 1);
210
211 request.onsuccess = (event) => {
212 const db = event.target.result;
213 const transaction = db.transaction(['syncedData'], 'readwrite');
214 const store = transaction.objectStore('syncedData');
215
216 store.put({ id: 'latest', data: data, timestamp: Date.now() });
217
218 transaction.oncomplete = () => resolve();
219 transaction.onerror = () => reject(transaction.error);
220 };
221
222 request.onerror = () => reject(request.error);
223 });
224 }
225}
226
227new BackgroundTaskProcessor();Le Service Worker ne convient pas aux longs calculs, car le navigateur l'endort dès qu'il ne traite plus d'événements. Lors de la première visite, navigator.serviceWorker.controller vaut parfois null, d'où l'opérateur ?. dans le code.
Performance monitoring et debugging
Le moniteur remplace postMessage et onmessage pour enregistrer la durée de chaque tâche :
1// WorkerPerformanceMonitor.js
2class WorkerPerformanceMonitor {
3 constructor() {
4 this.metrics = {
5 taskExecutionTimes: [],
6 memoryUsage: [],
7 errorRates: {},
8 throughput: []
9 };
10
11 this.startTime = performance.now();
12 }
13
14 // Surveillance des performances du worker
15 monitorWorkerPerformance(worker, taskId) {
16 const startTime = performance.now();
17 let memoryBefore = null;
18
19 // Mesure de la mémoire (si disponible)
20 if (performance.memory) {
21 memoryBefore = performance.memory.usedJSHeapSize;
22 }
23
24 const originalPostMessage = worker.postMessage.bind(worker);
25 const originalOnMessage = worker.onmessage;
26
27 // Intercept postMessage
28 worker.postMessage = (message) => {
29 console.log(`[Worker] Sending task ${taskId}:`, message);
30 originalPostMessage(message);
31 };
32
33 // Intercept onmessage
34 worker.onmessage = (event) => {
35 const endTime = performance.now();
36 const executionTime = endTime - startTime;
37
38 // Enregistrement de la métrique de temps d'exécution
39 this.metrics.taskExecutionTimes.push({
40 taskId: taskId,
41 executionTime: executionTime,
42 timestamp: Date.now()
43 });
44
45 // Mesure de la mémoire après l'exécution
46 if (performance.memory && memoryBefore) {
47 const memoryAfter = performance.memory.usedJSHeapSize;
48 const memoryDelta = memoryAfter - memoryBefore;
49
50 this.metrics.memoryUsage.push({
51 taskId: taskId,
52 memoryDelta: memoryDelta,
53 timestamp: Date.now()
54 });
55 }
56
57 console.log(`[Worker] Task ${taskId} completed in ${executionTime.toFixed(2)}ms`);
58
59 if (originalOnMessage) {
60 originalOnMessage(event);
61 }
62 };
63
64 return worker;
65 }
66
67 // Analyse des performances
68 generatePerformanceReport() {
69 const report = {
70 totalRuntime: performance.now() - this.startTime,
71 taskCount: this.metrics.taskExecutionTimes.length,
72 averageExecutionTime: this.calculateAverageExecutionTime(),
73 memoryTrends: this.analyzeMemoryTrends(),
74 throughputAnalysis: this.calculateThroughput(),
75 recommendations: this.generateRecommendations()
76 };
77
78 return report;
79 }
80
81 calculateAverageExecutionTime() {
82 if (this.metrics.taskExecutionTimes.length === 0) return 0;
83
84 const total = this.metrics.taskExecutionTimes.reduce(
85 (sum, metric) => sum + metric.executionTime, 0
86 );
87
88 return total / this.metrics.taskExecutionTimes.length;
89 }
90
91 analyzeMemoryTrends() {
92 if (this.metrics.memoryUsage.length === 0) return null;
93
94 const memoryDeltas = this.metrics.memoryUsage.map(m => m.memoryDelta);
95 const averageMemoryDelta = memoryDeltas.reduce((a, b) => a + b, 0) / memoryDeltas.length;
96
97 return {
98 averageMemoryDelta: averageMemoryDelta,
99 maxMemoryDelta: Math.max(...memoryDeltas),
100 minMemoryDelta: Math.min(...memoryDeltas),
101 potentialMemoryLeak: averageMemoryDelta > 1024 * 1024 // > 1MB average
102 };
103 }
104
105 calculateThroughput() {
106 const timeWindow = 60000; // 1 minute
107 const now = Date.now();
108 const recentTasks = this.metrics.taskExecutionTimes.filter(
109 task => now - task.timestamp < timeWindow
110 );
111
112 return {
113 tasksPerMinute: recentTasks.length,
114 averageTaskTime: recentTasks.length > 0
115 ? recentTasks.reduce((sum, task) => sum + task.executionTime, 0) / recentTasks.length
116 : 0
117 };
118 }
119
120 generateRecommendations() {
121 const recommendations = [];
122 const avgTime = this.calculateAverageExecutionTime();
123 const memoryTrends = this.analyzeMemoryTrends();
124
125 if (avgTime > 5000) { // > 5 secondes
126 recommendations.push('Envisager de découper les tâches longues en parties plus petites');
127 }
128
129 if (memoryTrends?.potentialMemoryLeak) {
130 recommendations.push('Fuite de mémoire potentielle détectée - vérifier la gestion de la mémoire');
131 }
132
133 if (this.metrics.taskExecutionTimes.length > 1000) {
134 recommendations.push("Grand nombre de tâches - envisager d'implémenter un Worker Pool");
135 }
136
137 return recommendations;
138 }
139}
140
141// Utilisation du monitoring
142const performanceMonitor = new WorkerPerformanceMonitor();
143
144// Surveillance d'un worker précis
145const worker = new Worker('calculation-worker.js');
146const monitoredWorker = performanceMonitor.monitorWorkerPerformance(worker, 'task_001');
147
148// Génération du rapport au bout d'un certain temps
149setTimeout(() => {
150 const report = performanceMonitor.generatePerformanceReport();
151 console.log('Performance Report:', report);
152}, 30000);performance.memory est une API non standard disponible uniquement dans Chromium, alors considère ces mesures comme indicatives.
Best practices et optimisations
Une communication optimale avec les workers
Chaque message a un coût de copie, donc on regroupe les petits messages en lots, on compresse les gros et on transmet les tampons sans copie :
1// OptimizedWorkerCommunication.js
2class OptimizedWorkerCommunication {
3 constructor() {
4 this.messageQueue = [];
5 this.batchSize = 10;
6 this.batchTimeout = 16; // ~60fps
7 this.compressionThreshold = 1024; // 1KB
8 }
9
10 // Batching des messages pour de meilleures performances
11 sendBatchedMessage(worker, message) {
12 this.messageQueue.push({ worker, message });
13
14 if (this.messageQueue.length >= this.batchSize) {
15 this.flushMessageQueue();
16 } else {
17 // Flush après le timeout si le lot n'est pas plein
18 setTimeout(() => {
19 if (this.messageQueue.length > 0) {
20 this.flushMessageQueue();
21 }
22 }, this.batchTimeout);
23 }
24 }
25
26 flushMessageQueue() {
27 const workerMessages = new Map();
28
29 // Regroupement des messages par worker
30 this.messageQueue.forEach(({ worker, message }) => {
31 if (!workerMessages.has(worker)) {
32 workerMessages.set(worker, []);
33 }
34 workerMessages.get(worker).push(message);
35 });
36
37 // Envoi des messages regroupés en lots
38 workerMessages.forEach((messages, worker) => {
39 worker.postMessage({
40 type: 'BATCH_MESSAGES',
41 messages: messages,
42 timestamp: Date.now()
43 });
44 });
45
46 this.messageQueue = [];
47 }
48
49 // Compression des grosses données avant l'envoi
50 async sendCompressedData(worker, data) {
51 const serializedData = JSON.stringify(data);
52
53 if (serializedData.length > this.compressionThreshold) {
54 try {
55 // Utilisation de la Compression Streams API (si disponible)
56 const compressedData = await this.compressData(serializedData);
57
58 worker.postMessage({
59 type: 'COMPRESSED_DATA',
60 data: compressedData,
61 compressed: true,
62 originalSize: serializedData.length
63 });
64 } catch (error) {
65 // Repli sur les données non compressées
66 worker.postMessage({
67 type: 'RAW_DATA',
68 data: data,
69 compressed: false
70 });
71 }
72 } else {
73 worker.postMessage({
74 type: 'RAW_DATA',
75 data: data,
76 compressed: false
77 });
78 }
79 }
80
81 async compressData(data) {
82 // Implémentation de la compression (exemple avec Compression Streams)
83 if ('CompressionStream' in window) {
84 const stream = new CompressionStream('gzip');
85 const writer = stream.writable.getWriter();
86 const reader = stream.readable.getReader();
87
88 writer.write(new TextEncoder().encode(data));
89 writer.close();
90
91 const chunks = [];
92 let done = false;
93
94 while (!done) {
95 const { value, done: readerDone } = await reader.read();
96 done = readerDone;
97 if (value) {
98 chunks.push(value);
99 }
100 }
101
102 return new Uint8Array(chunks.reduce((acc, chunk) => [...acc, ...chunk], []));
103 }
104
105 // Repli - on renvoie les données d'origine
106 return new TextEncoder().encode(data);
107 }
108
109 // Transferable Objects pour un transfert efficace
110 sendTransferableData(worker, arrayBuffer) {
111 worker.postMessage({
112 type: 'TRANSFERABLE_DATA',
113 data: arrayBuffer
114 }, [arrayBuffer]); // Transfer ownership
115
116 console.log('Transferred ArrayBuffer to worker (zero-copy)');
117 }
118}postMessage(data, [arrayBuffer]) transmet le tampon comme objet transférable (Transferable) : au lieu d'une copie, c'est le propriétaire qui change, et dans le thread principal, le tampon a ensuite une longueur de 0.
Error handling et résilience
Un worker peut tomber à cause d'une erreur dans son script. Un gestionnaire résilient relance les tâches avec un délai croissant (exponential backoff), surveille les délais d'expiration et recrée les workers morts :
1// ResilientWorkerManager.js
2class ResilientWorkerManager {
3 constructor(workerScript, options = {}) {
4 this.workerScript = workerScript;
5 this.options = {
6 maxRetries: 3,
7 retryDelay: 1000,
8 healthCheckInterval: 30000,
9 taskTimeout: 60000,
10 ...options
11 };
12
13 this.workers = new Map();
14 this.failedTasks = new Map();
15 this.healthCheckTimer = null;
16
17 this.startHealthChecking();
18 }
19
20 // Création d'un worker avec gestion des erreurs
21 createResilientWorker(workerId) {
22 const worker = new Worker(this.workerScript);
23 const workerInfo = {
24 instance: worker,
25 id: workerId,
26 healthy: true,
27 taskCount: 0,
28 errorCount: 0,
29 lastActivity: Date.now(),
30 activeTasks: new Map()
31 };
32
33 // Error handling
34 worker.onerror = (error) => {
35 this.handleWorkerError(workerInfo, error);
36 };
37
38 // Gestion des messages avec timeout
39 worker.onmessage = (event) => {
40 this.handleWorkerMessage(workerInfo, event.data);
41 };
42
43 this.workers.set(workerId, workerInfo);
44 return workerInfo;
45 }
46
47 // Exécution d'une tâche avec logique de retry
48 async executeTaskWithRetry(taskData, workerId = null) {
49 let attempts = 0;
50 let lastError = null;
51
52 while (attempts < this.options.maxRetries) {
53 try {
54 const result = await this.executeTask(taskData, workerId);
55
56 // Remise à zéro du compteur de tâches échouées après un succès
57 if (this.failedTasks.has(taskData.id)) {
58 this.failedTasks.delete(taskData.id);
59 }
60
61 return result;
62 } catch (error) {
63 attempts++;
64 lastError = error;
65
66 console.warn(`Task ${taskData.id} failed (attempt ${attempts}/${this.options.maxRetries}):`, error);
67
68 if (attempts < this.options.maxRetries) {
69 // Exponential backoff
70 const delay = this.options.retryDelay * Math.pow(2, attempts - 1);
71 await this.sleep(delay);
72
73 // Recréer le worker si nécessaire
74 if (error.type === 'WORKER_DIED') {
75 await this.recreateWorker(workerId);
76 }
77 }
78 }
79 }
80
81 // Enregistrement de la tâche comme échouée
82 this.failedTasks.set(taskData.id, {
83 taskData: taskData,
84 attempts: attempts,
85 lastError: lastError,
86 failedAt: Date.now()
87 });
88
89 throw new Error(`Task ${taskData.id} failed after ${attempts} attempts: ${lastError.message}`);
90 }
91
92 executeTask(taskData, workerId) {
93 return new Promise((resolve, reject) => {
94 const worker = workerId ? this.workers.get(workerId) : this.getHealthyWorker();
95
96 if (!worker || !worker.healthy) {
97 reject(new Error('No healthy worker available'));
98 return;
99 }
100
101 const taskId = taskData.id || this.generateTaskId();
102 const timeoutId = setTimeout(() => {
103 worker.activeTasks.delete(taskId);
104 reject(new Error(`Task ${taskId} timed out`));
105 }, this.options.taskTimeout);
106
107 // Enregistrement de la tâche comme active
108 worker.activeTasks.set(taskId, {
109 resolve: resolve,
110 reject: reject,
111 timeoutId: timeoutId,
112 startTime: Date.now()
113 });
114
115 worker.taskCount++;
116 worker.lastActivity = Date.now();
117
118 worker.instance.postMessage({
119 ...taskData,
120 taskId: taskId
121 });
122 });
123 }
124
125 handleWorkerMessage(workerInfo, message) {
126 const { taskId, type, result, error } = message;
127
128 if (workerInfo.activeTasks.has(taskId)) {
129 const task = workerInfo.activeTasks.get(taskId);
130
131 clearTimeout(task.timeoutId);
132 workerInfo.activeTasks.delete(taskId);
133 workerInfo.lastActivity = Date.now();
134
135 if (type === 'TASK_SUCCESS') {
136 task.resolve(result);
137 } else if (type === 'TASK_ERROR') {
138 workerInfo.errorCount++;
139 task.reject(new Error(error));
140 }
141 }
142 }
143
144 handleWorkerError(workerInfo, error) {
145 console.error(`Worker ${workerInfo.id} error:`, error);
146
147 workerInfo.healthy = false;
148 workerInfo.errorCount++;
149
150 // Rejet de toutes les tâches actives
151 workerInfo.activeTasks.forEach(task => {
152 clearTimeout(task.timeoutId);
153 task.reject(new Error('Worker died'));
154 });
155
156 workerInfo.activeTasks.clear();
157
158 // Auto-restart worker
159 setTimeout(() => {
160 this.recreateWorker(workerInfo.id);
161 }, this.options.retryDelay);
162 }
163
164 async recreateWorker(workerId) {
165 const oldWorker = this.workers.get(workerId);
166
167 if (oldWorker) {
168 oldWorker.instance.terminate();
169 this.workers.delete(workerId);
170 }
171
172 console.log(`Recreating worker ${workerId}`);
173 const newWorker = this.createResilientWorker(workerId);
174
175 // Health check pour le nouveau worker
176 await this.waitForWorkerReady(newWorker);
177 }
178
179 waitForWorkerReady(workerInfo, timeout = 5000) {
180 return new Promise((resolve, reject) => {
181 const timeoutId = setTimeout(() => {
182 reject(new Error(`Worker ${workerInfo.id} failed to initialize`));
183 }, timeout);
184
185 // Ping test
186 const pingTest = () => {
187 workerInfo.instance.postMessage({
188 type: 'PING',
189 timestamp: Date.now()
190 });
191 };
192
193 const handlePong = (event) => {
194 if (event.data.type === 'PONG') {
195 clearTimeout(timeoutId);
196 workerInfo.instance.removeEventListener('message', handlePong);
197 workerInfo.healthy = true;
198 resolve(workerInfo);
199 }
200 };
201
202 workerInfo.instance.addEventListener('message', handlePong);
203 pingTest();
204 });
205 }
206
207 // Vérification périodique de la santé des workers
208 startHealthChecking() {
209 this.healthCheckTimer = setInterval(() => {
210 this.performHealthCheck();
211 }, this.options.healthCheckInterval);
212 }
213
214 performHealthCheck() {
215 this.workers.forEach((workerInfo, workerId) => {
216 const timeSinceLastActivity = Date.now() - workerInfo.lastActivity;
217
218 // Vérifier que le worker répond
219 if (timeSinceLastActivity > this.options.healthCheckInterval * 2) {
220 console.warn(`Worker ${workerId} appears unresponsive`);
221 this.recreateWorker(workerId);
222 }
223
224 // Vérification du taux d'erreur
225 const errorRate = workerInfo.errorCount / Math.max(workerInfo.taskCount, 1);
226 if (errorRate > 0.1) { // > 10% error rate
227 console.warn(`Worker ${workerId} has high error rate: ${(errorRate * 100).toFixed(2)}%`);
228 }
229 });
230 }
231
232 getHealthyWorker() {
233 const healthyWorkers = Array.from(this.workers.values()).filter(w => w.healthy);
234
235 if (healthyWorkers.length === 0) {
236 return null;
237 }
238
239 // Choix du worker qui a le moins de tâches actives
240 return healthyWorkers.reduce((best, current) =>
241 current.activeTasks.size < best.activeTasks.size ? current : best
242 );
243 }
244
245 sleep(ms) {
246 return new Promise(resolve => setTimeout(resolve, ms));
247 }
248
249 generateTaskId() {
250 return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
251 }
252
253 // Nettoyage des ressources
254 terminate() {
255 if (this.healthCheckTimer) {
256 clearInterval(this.healthCheckTimer);
257 }
258
259 this.workers.forEach(workerInfo => {
260 workerInfo.instance.terminate();
261 });
262
263 this.workers.clear();
264 }
265
266 // Statistiques du système
267 getSystemStats() {
268 const workers = Array.from(this.workers.values());
269
270 return {
271 totalWorkers: workers.length,
272 healthyWorkers: workers.filter(w => w.healthy).length,
273 totalTasks: workers.reduce((sum, w) => sum + w.taskCount, 0),
274 activeTasks: workers.reduce((sum, w) => sum + w.activeTasks.size, 0),
275 totalErrors: workers.reduce((sum, w) => sum + w.errorCount, 0),
276 failedTasksCount: this.failedTasks.size,
277 averageErrorRate: workers.length > 0
278 ? (workers.reduce((sum, w) => sum + (w.errorCount / Math.max(w.taskCount, 1)), 0) / workers.length * 100).toFixed(2) + '%'
279 : '0%'
280 };
281 }
282}Le délai d'expiration de chaque tâche protège contre un worker bloqué, et PING et PONG vérifient que le nouveau worker répond.
Exemple pratique : le rendu 3D
Pour finir, un moteur de rendu découpe l'écran en tuiles de 64 sur 64 pixels et les confie à un pool de workers, qui calculent les couleurs par lancer de rayons (ray tracing) :
1// 3DRenderingApp.js
2class WebWorker3DRenderer {
3 constructor(canvasId) {
4 this.canvas = document.getElementById(canvasId);
5 this.ctx = this.canvas.getContext('2d');
6 this.workerPool = new WorkerPool('3d-renderer-worker.js', 4);
7
8 this.scene = {
9 objects: [],
10 camera: { x: 0, y: 0, z: -10 },
11 lights: [{ x: 5, y: 5, z: -5, intensity: 1 }]
12 };
13
14 this.setupScene();
15 }
16
17 setupScene() {
18 // Ajout d'objets 3D à la scène
19 this.scene.objects = [
20 { type: 'cube', position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 } },
21 { type: 'sphere', position: { x: 3, y: 0, z: 2 }, radius: 1 },
22 { type: 'cylinder', position: { x: -3, y: 0, z: 1 }, height: 2, radius: 0.5 }
23 ];
24 }
25
26 async render() {
27 const startTime = performance.now();
28
29 // Découpage de l'écran en tiles pour un rendu parallèle
30 const tileSize = 64;
31 const tilesX = Math.ceil(this.canvas.width / tileSize);
32 const tilesY = Math.ceil(this.canvas.height / tileSize);
33
34 const renderPromises = [];
35
36 for (let tileY = 0; tileY < tilesY; tileY++) {
37 for (let tileX = 0; tileX < tilesX; tileX++) {
38 const tileData = {
39 type: 'RENDER_TILE',
40 tile: {
41 x: tileX * tileSize,
42 y: tileY * tileSize,
43 width: Math.min(tileSize, this.canvas.width - tileX * tileSize),
44 height: Math.min(tileSize, this.canvas.height - tileY * tileSize)
45 },
46 scene: this.scene,
47 screenDimensions: {
48 width: this.canvas.width,
49 height: this.canvas.height
50 }
51 };
52
53 renderPromises.push(
54 this.workerPool.execute(tileData).then(result => ({
55 ...result,
56 tileX: tileX,
57 tileY: tileY
58 }))
59 );
60 }
61 }
62
63 // Attente de toutes les tiles
64 const renderedTiles = await Promise.all(renderPromises);
65
66 // Assemblage des tiles sur le canvas
67 renderedTiles.forEach(tile => {
68 const imageData = new ImageData(
69 new Uint8ClampedArray(tile.pixelData),
70 tile.width,
71 tile.height
72 );
73
74 this.ctx.putImageData(imageData, tile.x, tile.y);
75 });
76
77 const renderTime = performance.now() - startTime;
78 console.log(`Frame rendered in ${renderTime.toFixed(2)}ms`);
79 }
80
81 // Animation de la scène
82 animate() {
83 this.scene.objects.forEach(obj => {
84 if (obj.rotation) {
85 obj.rotation.y += 0.02;
86 obj.rotation.x += 0.01;
87 }
88 });
89
90 this.render().then(() => {
91 requestAnimationFrame(() => this.animate());
92 });
93 }
94}
95
96// 3d-renderer-worker.js
97class Tile3DRenderer {
98 constructor() {
99 self.onmessage = (event) => {
100 this.handleRenderTask(event.data);
101 };
102 }
103
104 handleRenderTask(data) {
105 if (data.type === 'RENDER_TILE') {
106 const result = this.renderTile(data.tile, data.scene, data.screenDimensions);
107
108 self.postMessage({
109 type: 'TASK_COMPLETE',
110 result: result
111 });
112 }
113 }
114
115 renderTile(tile, scene, screenDimensions) {
116 const { x, y, width, height } = tile;
117 const pixelData = new Uint8ClampedArray(width * height * 4);
118
119 for (let py = 0; py < height; py++) {
120 for (let px = 0; px < width; px++) {
121 const screenX = x + px;
122 const screenY = y + py;
123
124 // Conversion des coordonnées de l'écran en rayon
125 const ray = this.screenToRay(screenX, screenY, screenDimensions, scene.camera);
126
127 // Ray tracing
128 const color = this.traceRay(ray, scene);
129
130 const pixelIndex = (py * width + px) * 4;
131 pixelData[pixelIndex] = color.r; // Red
132 pixelData[pixelIndex + 1] = color.g; // Green
133 pixelData[pixelIndex + 2] = color.b; // Blue
134 pixelData[pixelIndex + 3] = 255; // Alpha
135 }
136 }
137
138 return {
139 x: x,
140 y: y,
141 width: width,
142 height: height,
143 pixelData: Array.from(pixelData)
144 };
145 }
146
147 screenToRay(screenX, screenY, screenDimensions, camera) {
148 const normalizedX = (screenX / screenDimensions.width) * 2 - 1;
149 const normalizedY = 1 - (screenY / screenDimensions.height) * 2;
150
151 return {
152 origin: camera,
153 direction: {
154 x: normalizedX,
155 y: normalizedY,
156 z: 1
157 }
158 };
159 }
160
161 traceRay(ray, scene) {
162 let closestDistance = Infinity;
163 let hitColor = { r: 0, g: 0, b: 50 }; // Arrière-plan
164
165 // Détection des collisions avec les objets
166 scene.objects.forEach(obj => {
167 const distance = this.rayObjectIntersection(ray, obj);
168
169 if (distance > 0 && distance < closestDistance) {
170 closestDistance = distance;
171 hitColor = this.calculateObjectColor(obj, ray, distance, scene.lights);
172 }
173 });
174
175 return hitColor;
176 }
177
178 rayObjectIntersection(ray, obj) {
179 switch (obj.type) {
180 case 'sphere':
181 return this.raySphereIntersection(ray, obj);
182 case 'cube':
183 return this.rayCubeIntersection(ray, obj);
184 default:
185 return -1;
186 }
187 }
188
189 raySphereIntersection(ray, sphere) {
190 const dx = ray.origin.x - sphere.position.x;
191 const dy = ray.origin.y - sphere.position.y;
192 const dz = ray.origin.z - sphere.position.z;
193
194 const a = ray.direction.x * ray.direction.x +
195 ray.direction.y * ray.direction.y +
196 ray.direction.z * ray.direction.z;
197
198 const b = 2 * (dx * ray.direction.x + dy * ray.direction.y + dz * ray.direction.z);
199 const c = dx * dx + dy * dy + dz * dz - sphere.radius * sphere.radius;
200
201 const discriminant = b * b - 4 * a * c;
202
203 if (discriminant < 0) return -1;
204
205 const t1 = (-b - Math.sqrt(discriminant)) / (2 * a);
206 const t2 = (-b + Math.sqrt(discriminant)) / (2 * a);
207
208 return t1 > 0 ? t1 : (t2 > 0 ? t2 : -1);
209 }
210
211 calculateObjectColor(obj, ray, distance, lights) {
212 // Éclairage simplifié
213 const baseColor = obj.type === 'sphere'
214 ? { r: 255, g: 100, b: 100 }
215 : { r: 100, g: 255, b: 100 };
216
217 // Simulation de l'éclairage
218 const lightIntensity = 0.7 + 0.3 * Math.sin(distance * 0.1);
219
220 return {
221 r: Math.floor(baseColor.r * lightIntensity),
222 g: Math.floor(baseColor.g * lightIntensity),
223 b: Math.floor(baseColor.b * lightIntensity)
224 };
225 }
226}
227
228new Tile3DRenderer();Le thread principal se contente d'assembler les tuiles sur le canvas. Ce serait plus rapide de transmettre les tampons de pixels comme Transferable au lieu de les convertir en tableaux avec Array.from.
Récapitulatif
Les Web Workers permettent :
- D'exécuter des calculs lourds sans bloquer l'UI
- De traiter en parallèle de grands ensembles de données
- D'améliorer la réactivité des applications web
- De monter en charge grâce au Worker Pool
- De faire du background processing avec les Service Workers
Tu y auras recours pour les calculs mathématiques et scientifiques, le traitement d'images et de vidéos, le rendu 3D, le tri et le filtrage de grands ensembles de données, la compression, la cryptographie et le hachage, ainsi que pour exécuter des modèles de machine learning. Commence par un seul worker en mode module (new Worker('worker.js', { type: 'module' })), et ne construis un pool que lorsque les mesures montrent que cela ne suffit plus. Dans l'éditeur ci-dessous t'attend une simulation de worker, et dans la leçon suivante, nous jetterons un œil à la mémoire et au garbage collector.
Retiens : un worker est un laboratoire séparé derrière une vitre, qui travaille en parallèle, mais avec lequel tu ne communiques que par le guichet postMessage.
Code de cette leçon : index.js
1// Web Workers - exemple de calcul de nombres premiers
2// Thread principal
3console.log("Jurassic Park - Système d'analyse ADN");
4console.log("Démarrage de calculs lourds sans bloquer l'UI\n");
5
6// Simulation d'un Web Worker (un vrai worker nécessite un fichier séparé)
7function simulateWorker() {
8 // Fonction qui vérifie si un nombre est premier
9 function isPrime(n) {
10 if (n <= 1) return false;
11 if (n <= 3) return true;
12 if (n % 2 === 0 || n % 3 === 0) return false;
13
14 for (let i = 5; i * i <= n; i += 6) {
15 if (n % i === 0 || n % (i + 2) === 0) return false;
16 }
17 return true;
18 }
19
20 // Trouver les nombres premiers dans une plage
21 function findPrimes(start, end) {
22 const primes = [];
23 for (let i = start; i <= end; i++) {
24 if (isPrime(i)) {
25 primes.push(i);
26 }
27 }
28 return primes;
29 }
30
31 return findPrimes;
32}
33
34// Fonction principale
35async function calculatePrimesInBackground() {
36 console.log("Je démarre les calculs en arrière-plan...");
37 console.log("L'UI reste réactive !\n");
38
39 const findPrimes = simulateWorker();
40
41 // Simulation de calculs longs
42 const start = 1;
43 const end = 10000;
44
45 console.time("Calculation time");
46 const primes = findPrimes(start, end);
47 console.timeEnd("Calculation time");
48
49 console.log(`\nTrouvé ${primes.length} nombres premiers`);
50 console.log(`Les 20 premiers : ${primes.slice(0, 20).join(', ')}`);
51 console.log(`Les 20 derniers : ${primes.slice(-20).join(', ')}`);
52}
53
54// Structure de l'API Web Worker (pour référence)
55console.log("=== Structure de l'API Web Worker ===");
56console.log("1. Créer un worker :");
57console.log(" const worker = new Worker('worker.js');");
58console.log("\n2. Envoyer des données au worker :");
59console.log(" worker.postMessage({ action: 'calculate', data: 100 });");
60console.log("\n3. Recevoir les résultats :");
61console.log(" worker.onmessage = (event) => console.log(event.data);");
62console.log("\n4. Gestion des erreurs :");
63console.log(" worker.onerror = (error) => console.error(error);");
64console.log("\n5. Fermer le worker :");
65console.log(" worker.terminate();\n");
66
67// Lance les calculs
68calculatePrimesInBackground();
69
70// Démonstration du fonctionnement simultané de l'UI
71console.log("\nL'UI fonctionne normalement pendant les calculs !");
72console.log("D'autres opérations peuvent être effectuées...");
73console.log("Clics, animations, tout reste réactif !");
74
75// Exemple de « worker » avec une promesse
76class PrimeWorkerSimulator {
77 calculate(start, end) {
78 return new Promise((resolve) => {
79 setTimeout(() => {
80 const primes = [];
81 for (let i = start; i <= end; i++) {
82 if (this.isPrime(i)) primes.push(i);
83 }
84 resolve(primes);
85 }, 0); // Async execution
86 });
87 }
88
89 isPrime(n) {
90 if (n <= 1) return false;
91 if (n <= 3) return true;
92 if (n % 2 === 0 || n % 3 === 0) return false;
93
94 for (let i = 5; i * i <= n; i += 6) {
95 if (n % i === 0 || n % (i + 2) === 0) return false;
96 }
97 return true;
98 }
99}
100
101// Test avec un worker basé sur une promesse
102const workerSim = new PrimeWorkerSimulator();
103workerSim.calculate(10000, 11000).then(primes => {
104 console.log(`\nCalculs terminés ! Trouvé ${primes.length} nombres premiers entre 10000 et 11000`);
105});Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quel est le principal problème que résolvent les Web Workers ?
2. Quelle méthode utilise-t-on pour envoyer des données à un Web Worker ?
Voici 2 des 5 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Crée un worker pour calculer des nombres premiers.
- Clique dans l'ordre
Ordonne les étapes de la communication avec un Web Worker.
- Tri horizontal
Ordonne les éléments pour créer un Web Worker :
- Clique dans l'ordre
Ordonne dans le bon ordre les éléments de création d'un Web Worker :
- Éditeur de code
Utilise SharedArrayBuffer pour la communication entre threads.
- Tri vertical
Ordonne dans le bon ordre les étapes du cycle de vie d'un Web Worker :
- Clique dans l'ordre
Ordonne les éléments de l'envoi d'un message à un Worker :