Web Workers : Comment Utiliser des Threads en JavaScript Pour Améliorer la Performance
Salut HaWkers, JavaScript est single-threaded par design. Cela signifie que les opérations lourdes bloquent le thread principal, causant des freezes de l'interface. Les Web Workers résolvent ce problème en permettant d'exécuter du code sur des threads séparés.
Explorons comment utiliser les Web Workers pour créer des applications plus réactives et performantes.
Le Problème du Thread Principal
Le thread principal de JavaScript est responsable de :
- Exécuter votre code
- Traiter les événements du DOM
- Rendre les mises à jour visuelles
- Répondre aux interactions utilisateur
Quand vous exécutez du code lourd, toutes ces tâches sont bloquées :
// ❌ MAUVAIS - Bloque le thread principal
function traiterDonneesLourdes(donnees) {
// Simulation de traitement lourd
const resultat = [];
for (let i = 0; i < 10000000; i++) {
resultat.push(Math.sqrt(donnees[i % donnees.length]) * Math.random());
}
return resultat;
}
// Pendant cette exécution, l'UI est gelée
// Les clics ne répondent pas, les animations s'arrêtent
const resultat = traiterDonneesLourdes(mesDonnees);Symptômes d'un thread principal bloqué :
- Interface gelée
- Animations en pause
- Inputs qui ne répondent pas
- Scroll saccadé
- "Page Unresponsive" dans le navigateur
Comment les Web Workers Fonctionnent
Les Web Workers exécutent du JavaScript sur un thread séparé, complètement isolé du thread principal.
Communication via Messages
Les workers communiquent avec le thread principal via des messages :
// main.js - Thread principal
const worker = new Worker('worker.js');
// Envoyer des données au worker
worker.postMessage({ donnees: mesDonnees, type: 'traiter' });
// Recevoir le résultat du worker
worker.onmessage = (event) => {
console.log('Résultat:', event.data);
// Mettre à jour l'UI avec le résultat
mettreAJourInterface(event.data);
};
// Gérer les erreurs
worker.onerror = (error) => {
console.error('Erreur dans le worker:', error.message);
};// worker.js - Thread séparé
self.onmessage = (event) => {
const { donnees, type } = event.data;
if (type === 'traiter') {
// Traitement lourd - ne bloque pas l'UI
const resultat = traiterDonnees(donnees);
// Envoyer le résultat
self.postMessage(resultat);
}
};
function traiterDonnees(donnees) {
const resultat = [];
for (let i = 0; i < 10000000; i++) {
resultat.push(Math.sqrt(donnees[i % donnees.length]) * Math.random());
}
return resultat;
}
Types de Workers
Dedicated Workers
Le type le plus courant. Exclusif à un script spécifique :
// Créer un worker dédié
const worker = new Worker('worker.js');
// Terminer le worker quand plus nécessaire
worker.terminate();Shared Workers
Partagé entre plusieurs tabs/fenêtres de la même origine :
// main.js
const sharedWorker = new SharedWorker('shared-worker.js');
sharedWorker.port.onmessage = (event) => {
console.log('Message reçu:', event.data);
};
sharedWorker.port.postMessage('Hello from tab');// shared-worker.js
const connections = [];
self.onconnect = (event) => {
const port = event.ports[0];
connections.push(port);
port.onmessage = (event) => {
// Broadcast à toutes les connexions
connections.forEach(conn => {
conn.postMessage(`Broadcast: ${event.data}`);
});
};
port.start();
};Service Workers
Pour le cache, les push notifications et la fonctionnalité offline (non couvert en détail ici).
Cas d'Usage Pratiques
1. Traitement d'Images
// main.js
const imageWorker = new Worker('image-worker.js');
async function traiterImage(imageFile) {
const bitmap = await createImageBitmap(imageFile);
// Utiliser OffscreenCanvas pour traiter dans le worker
const canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const offscreen = canvas.transferControlToOffscreen();
// Transférer le canvas au worker (ne copie pas, transfère)
imageWorker.postMessage(
{ canvas: offscreen, bitmap },
[offscreen, bitmap]
);
}
imageWorker.onmessage = (event) => {
if (event.data.type === 'progres') {
mettreAJourBarreProgression(event.data.pourcentage);
} else if (event.data.type === 'complete') {
afficherResultat(event.data.imageData);
}
};// image-worker.js
self.onmessage = async (event) => {
const { canvas, bitmap } = event.data;
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Appliquer un filtre (exemple : grayscale)
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
const avg = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
pixels[i] = avg; // R
pixels[i + 1] = avg; // G
pixels[i + 2] = avg; // B
// Reporter le progrès
if (i % 100000 === 0) {
self.postMessage({
type: 'progres',
pourcentage: Math.round((i / pixels.length) * 100)
});
}
}
ctx.putImageData(imageData, 0, 0);
self.postMessage({
type: 'complete',
imageData: ctx.getImageData(0, 0, canvas.width, canvas.height)
});
};2. Parsing de Fichiers Volumineux
// main.js
const csvWorker = new Worker('csv-worker.js');
async function traiterCSV(file) {
const text = await file.text();
csvWorker.postMessage({ csv: text, options: { delimiter: ',' } });
}
csvWorker.onmessage = (event) => {
const { type, donnees, erreur } = event.data;
if (type === 'succes') {
console.log(`Traitées ${donnees.length} lignes`);
afficherTableau(donnees);
} else if (type === 'erreur') {
console.error('Erreur lors du traitement CSV:', erreur);
}
};// csv-worker.js
self.onmessage = (event) => {
try {
const { csv, options } = event.data;
const lignes = csv.split('\n');
const headers = lignes[0].split(options.delimiter);
const donnees = [];
for (let i = 1; i < lignes.length; i++) {
if (!lignes[i].trim()) continue;
const valeurs = lignes[i].split(options.delimiter);
const objet = {};
headers.forEach((header, index) => {
objet[header.trim()] = valeurs[index]?.trim();
});
donnees.push(objet);
}
self.postMessage({ type: 'succes', donnees });
} catch (erreur) {
self.postMessage({ type: 'erreur', erreur: erreur.message });
}
};
3. Calculs Mathématiques Complexes
// main.js
const mathWorker = new Worker('math-worker.js');
function calculerStatistiques(nombres) {
return new Promise((resolve, reject) => {
mathWorker.postMessage({ nombres });
mathWorker.onmessage = (event) => {
resolve(event.data);
};
mathWorker.onerror = (error) => {
reject(error);
};
});
}
// Utilisation
const stats = await calculerStatistiques(grandTableauDeNombres);
console.log('Moyenne:', stats.moyenne);
console.log('Écart-type:', stats.ecartType);// math-worker.js
self.onmessage = (event) => {
const { nombres } = event.data;
// Moyenne
const somme = nombres.reduce((acc, n) => acc + n, 0);
const moyenne = somme / nombres.length;
// Variance
const sommeCarres = nombres.reduce((acc, n) => {
return acc + Math.pow(n - moyenne, 2);
}, 0);
const variance = sommeCarres / nombres.length;
// Écart-type
const ecartType = Math.sqrt(variance);
// Médiane
const sorted = [...nombres].sort((a, b) => a - b);
const milieu = Math.floor(sorted.length / 2);
const mediane = sorted.length % 2
? sorted[milieu]
: (sorted[milieu - 1] + sorted[milieu]) / 2;
self.postMessage({
moyenne,
mediane,
variance,
ecartType,
min: Math.min(...nombres),
max: Math.max(...nombres),
});
};
Worker avec TypeScript et Bundlers
Configuration avec Vite
// src/workers/data-processor.worker.ts
self.onmessage = (event: MessageEvent<{ data: number[] }>) => {
const { data } = event.data;
const result = data.map(n => n * 2);
self.postMessage(result);
};
export {}; // Nécessaire pour que TypeScript traite comme module// src/main.ts
// Vite supporte les workers nativement avec ?worker
import DataWorker from './workers/data-processor.worker?worker';
const worker = new DataWorker();
worker.postMessage({ data: [1, 2, 3, 4, 5] });
worker.onmessage = (event) => {
console.log('Résultat:', event.data); // [2, 4, 6, 8, 10]
};Configuration avec Webpack
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.worker\.ts$/,
use: {
loader: 'worker-loader',
options: {
filename: '[name].[contenthash].worker.js',
},
},
},
],
},
};
Transferable Objects
Pour les données volumineuses, utilisez les Transferable Objects pour éviter les copies :
// main.js
const buffer = new ArrayBuffer(1024 * 1024 * 100); // 100MB
// ❌ Copie les données (lent pour données volumineuses)
worker.postMessage({ buffer });
// ✅ Transfère l'ownership (instantané)
worker.postMessage({ buffer }, [buffer]);
// Après cela, `buffer` n'est plus disponible dans le thread principalTypes Transférables
// ArrayBuffer
const arrayBuffer = new ArrayBuffer(1024);
worker.postMessage({ data: arrayBuffer }, [arrayBuffer]);
// MessagePort
const channel = new MessageChannel();
worker.postMessage({ port: channel.port2 }, [channel.port2]);
// ImageBitmap
const bitmap = await createImageBitmap(imageBlob);
worker.postMessage({ image: bitmap }, [bitmap]);
// OffscreenCanvas
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);
Pool de Workers
Pour des tâches parallèles, utilisez un pool de workers :
// worker-pool.js
class WorkerPool {
constructor(workerScript, poolSize = navigator.hardwareConcurrency || 4) {
this.workers = [];
this.queue = [];
this.activeWorkers = new Set();
for (let i = 0; i < poolSize; i++) {
const worker = new Worker(workerScript);
worker.id = i;
this.workers.push(worker);
}
}
execute(data) {
return new Promise((resolve, reject) => {
const task = { data, resolve, reject };
const availableWorker = this.workers.find(
w => !this.activeWorkers.has(w.id)
);
if (availableWorker) {
this.runTask(availableWorker, task);
} else {
this.queue.push(task);
}
});
}
runTask(worker, task) {
this.activeWorkers.add(worker.id);
worker.onmessage = (event) => {
task.resolve(event.data);
this.activeWorkers.delete(worker.id);
this.processQueue();
};
worker.onerror = (error) => {
task.reject(error);
this.activeWorkers.delete(worker.id);
this.processQueue();
};
worker.postMessage(task.data);
}
processQueue() {
if (this.queue.length === 0) return;
const availableWorker = this.workers.find(
w => !this.activeWorkers.has(w.id)
);
if (availableWorker) {
const task = this.queue.shift();
this.runTask(availableWorker, task);
}
}
terminate() {
this.workers.forEach(w => w.terminate());
}
}
// Utilisation
const pool = new WorkerPool('processor.worker.js', 4);
const results = await Promise.all([
pool.execute({ chunk: data.slice(0, 1000) }),
pool.execute({ chunk: data.slice(1000, 2000) }),
pool.execute({ chunk: data.slice(2000, 3000) }),
pool.execute({ chunk: data.slice(3000, 4000) }),
]);
Limitations des Web Workers
Ce Qui N'est PAS Disponible
// worker.js
// ❌ Le DOM n'est pas disponible
document.getElementById('quelquechose'); // Error
// ❌ window n'existe pas
window.localStorage; // Error
// ❌ alert, confirm, prompt n'existent pas
alert('test'); // Error
// ✅ Mais vous pouvez utiliser :
fetch('/api/data'); // OK
setTimeout(() => {}, 1000); // OK
console.log('test'); // OK
crypto.randomUUID(); // OK
self.indexedDB; // OKWorkarounds Courants
// Si vous avez besoin de données du DOM, extrayez-les avant d'envoyer au worker
// main.js
const elementsData = Array.from(document.querySelectorAll('.item'))
.map(el => ({
texte: el.textContent,
dataset: { ...el.dataset }
}));
worker.postMessage({ elements: elementsData });Conclusion
Les Web Workers sont un outil puissant pour maintenir vos applications réactives. Utilisez-les pour :
- Traitement de données lourdes
- Parsing de fichiers volumineux
- Calculs mathématiques complexes
- Traitement d'images
- Toute opération qui prend plus de 50ms
Rappelez-vous des bonnes pratiques :
- Utilisez les Transferable Objects pour les données volumineuses
- Implémentez un pool de workers pour les tâches parallèles
- Gérez les erreurs correctement
- Terminez les workers quand vous n'en avez plus besoin
Si vous êtes intéressé par d'autres techniques de performance, je recommande de consulter l'article Debugging JavaScript : Techniques Avancées avec DevTools où nous explorons comment identifier les goulots d'étranglement de performance dans votre application.

