JavaScript ist single-threaded. Der Browser-Main-Thread erledigt alles: UI-Rendering, Event-Handling, JavaScript-Execution, Layout-Berechnung. Eine schwere Berechnung blockiert diesen Thread – die UI friert ein, Animationen stocken, Clicks werden ignoriert. Der User denkt: “App abgestürzt”.
Web Workers lösen dieses Problem durch echte Parallelität. Sie laufen in separaten Threads mit eigener Event-Loop, komplett isoliert vom Main Thread. Schwere Operationen wandern in den Worker, der Main Thread bleibt responsiv.
// ❌ Fibonacci-Berechnung blockiert UI
function Dashboard() {
const [result, setResult] = useState<number>(0);
const [loading, setLoading] = useState(false);
const calculate = () => {
setLoading(true);
// Rekursive Fibonacci - sehr langsam für große n
const fibonacci = (n: number): number => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
};
const result = fibonacci(45); // Dauert ~5-10 Sekunden
setResult(result);
setLoading(false);
};
return (
<div>
<button onClick={calculate}>Calculate Fibonacci(45)</button>
{loading && <p>Calculating...</p>}
<p>Result: {result}</p>
{/* Diese Animation stockt während Berechnung */}
<div className="spinner" />
</div>
);
}
Problem: Während fibonacci(45) läuft: -
UI reagiert nicht auf Klicks - Animationen frieren ein - Browser zeigt
“Page unresponsive” Warning - User denkt die App ist kaputt
Ein Web Worker ist ein separates JavaScript-File, das in eigenem Thread läuft. Kommunikation erfolgt über Messages – kein Shared Memory.
// workers/fibonacci.worker.ts
self.addEventListener('message', (event: MessageEvent<number>) => {
const n = event.data;
const fibonacci = (n: number): number => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
};
const result = fibonacci(n);
// Ergebnis zurück an Main Thread
self.postMessage(result);
});// Dashboard.tsx
import { useEffect, useState } from 'react';
function Dashboard() {
const [result, setResult] = useState<number>(0);
const [loading, setLoading] = useState(false);
const [worker, setWorker] = useState<Worker | null>(null);
useEffect(() => {
// Worker initialisieren
const fibWorker = new Worker(
new URL('./workers/fibonacci.worker.ts', import.meta.url),
{ type: 'module' }
);
// Message-Handler für Ergebnisse
fibWorker.onmessage = (event: MessageEvent<number>) => {
setResult(event.data);
setLoading(false);
};
setWorker(fibWorker);
// Cleanup
return () => fibWorker.terminate();
}, []);
const calculate = () => {
if (!worker) return;
setLoading(true);
worker.postMessage(45); // Berechnung in Worker
};
return (
<div>
<button onClick={calculate}>Calculate Fibonacci(45)</button>
{loading && <p>Calculating...</p>}
<p>Result: {result}</p>
{/* Animation läuft smooth weiter! */}
<div className="spinner" />
</div>
);
}
Vorteil: Main Thread bleibt frei: - UI bleibt responsiv - Animationen laufen smooth - User kann weiter interagieren - Berechnung läuft parallel
Dedicated Worker für eine spezifische Aufgabe. Jede Worker-Instanz ist isoliert.
// workers/computation.worker.ts
self.addEventListener('message', (event) => {
const result = heavyComputation(event.data);
self.postMessage(result);
});Ein Worker, der von mehreren Tabs/Windows geteilt wird.
// workers/shared.worker.ts
const connections: MessagePort[] = [];
self.addEventListener('connect', (event: MessageEvent) => {
const port = event.ports[0];
connections.push(port);
port.addEventListener('message', (e) => {
// Broadcast an alle Connections
connections.forEach(p => p.postMessage(e.data));
});
port.start();
});// App.tsx
const worker = new SharedWorker(
new URL('./workers/shared.worker.ts', import.meta.url),
{ type: 'module' }
);
worker.port.start();
worker.port.postMessage('Hello from Tab 1');
worker.port.onmessage = (e) => console.log(e.data);
Use Case: Sync zwischen Tabs (z.B. Chat-App, Real-time Notifications)
Service Worker sind für Offline-Funktionalität, Caching und Background-Sync gedacht – nicht für Berechnungen. Laufen auch wenn alle Tabs geschlossen sind.
// service-worker.ts
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then(cache => {
return cache.addAll(['/index.html', '/app.js']);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});Use Case: PWA, Offline-Support, Background-Sync – nicht für UI-Berechnungen.
Problem: postMessage und onmessage sind
any-typed. Lösung: Interfaces für Messages.
// types/worker-messages.ts
export interface WorkerRequest {
id: string;
type: 'fibonacci' | 'sort' | 'search';
payload: any;
}
export interface WorkerResponse {
id: string;
result: any;
error?: string;
}
// Spezifische Message-Typen
export interface FibonacciRequest {
id: string;
type: 'fibonacci';
payload: { n: number };
}
export interface FibonacciResponse {
id: string;
result: number;
}
export interface SortRequest {
id: string;
type: 'sort';
payload: { data: number[] };
}
export interface SortResponse {
id: string;
result: number[];
}// workers/computation.worker.ts
import type { WorkerRequest, WorkerResponse } from '../types/worker-messages';
self.addEventListener('message', (event: MessageEvent<WorkerRequest>) => {
const { id, type, payload } = event.data;
try {
let result: any;
switch (type) {
case 'fibonacci':
result = fibonacci(payload.n);
break;
case 'sort':
result = payload.data.sort((a: number, b: number) => a - b);
break;
case 'search':
result = payload.data.filter((item: any) =>
item.name.includes(payload.query)
);
break;
default:
throw new Error(`Unknown operation: ${type}`);
}
const response: WorkerResponse = { id, result };
self.postMessage(response);
} catch (error) {
const response: WorkerResponse = {
id,
result: null,
error: error instanceof Error ? error.message : 'Unknown error'
};
self.postMessage(response);
}
});
function fibonacci(n: number): number {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}// hooks/useWorker.ts
import { useEffect, useRef, useState } from 'react';
import type { WorkerRequest, WorkerResponse } from '../types/worker-messages';
interface UseWorkerOptions {
workerUrl: string;
}
export function useWorker({ workerUrl }: UseWorkerOptions) {
const workerRef = useRef<Worker | null>(null);
const [isReady, setIsReady] = useState(false);
// Pending requests Queue
const pendingRequests = useRef<Map<string, (result: any, error?: string) => void>>(
new Map()
);
useEffect(() => {
const worker = new Worker(new URL(workerUrl, import.meta.url), {
type: 'module'
});
worker.onmessage = (event: MessageEvent<WorkerResponse>) => {
const { id, result, error } = event.data;
const resolver = pendingRequests.current.get(id);
if (resolver) {
resolver(result, error);
pendingRequests.current.delete(id);
}
};
worker.onerror = (error) => {
console.error('Worker error:', error);
};
workerRef.current = worker;
setIsReady(true);
return () => {
worker.terminate();
pendingRequests.current.clear();
};
}, [workerUrl]);
const execute = async <T = any>(
type: WorkerRequest['type'],
payload: any
): Promise<T> => {
return new Promise((resolve, reject) => {
if (!workerRef.current || !isReady) {
reject(new Error('Worker not ready'));
return;
}
const id = crypto.randomUUID();
const request: WorkerRequest = { id, type, payload };
pendingRequests.current.set(id, (result, error) => {
if (error) {
reject(new Error(error));
} else {
resolve(result as T);
}
});
workerRef.current.postMessage(request);
});
};
return { execute, isReady };
}// Dashboard.tsx - Verwendung
import { useState } from 'react';
import { useWorker } from './hooks/useWorker';
function Dashboard() {
const [result, setResult] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
const { execute, isReady } = useWorker({
workerUrl: './workers/computation.worker.ts'
});
const calculate = async () => {
setLoading(true);
try {
const result = await execute<number>('fibonacci', { n: 45 });
setResult(result);
} catch (error) {
console.error('Calculation failed:', error);
} finally {
setLoading(false);
}
};
return (
<div>
<button onClick={calculate} disabled={!isReady || loading}>
Calculate Fibonacci(45)
</button>
{loading && <p>Calculating...</p>}
{result !== null && <p>Result: {result}</p>}
</div>
);
}
Reales Problem: 100.000 Produkte durchsuchen – blockiert UI ohne Worker.
// workers/search.worker.ts
interface Product {
id: number;
name: string;
category: string;
price: number;
}
interface SearchRequest {
id: string;
type: 'search';
payload: {
products: Product[];
query: string;
};
}
self.addEventListener('message', (event: MessageEvent<SearchRequest>) => {
const { id, payload } = event.data;
const { products, query } = payload;
const start = performance.now();
const results = products.filter(product =>
product.name.toLowerCase().includes(query.toLowerCase()) ||
product.category.toLowerCase().includes(query.toLowerCase())
);
const duration = performance.now() - start;
self.postMessage({
id,
result: { results, duration }
});
});// ProductSearch.tsx
import { useState, useCallback } from 'react';
import { useWorker } from './hooks/useWorker';
interface Product {
id: number;
name: string;
category: string;
price: number;
}
function ProductSearch({ products }: { products: Product[] }) {
const [query, setQuery] = useState('');
const [results, setResults] = useState<Product[]>([]);
const [searchTime, setSearchTime] = useState<number>(0);
const { execute } = useWorker({ workerUrl: './workers/search.worker.ts' });
const handleSearch = useCallback(async (searchQuery: string) => {
if (!searchQuery.trim()) {
setResults([]);
return;
}
const { results, duration } = await execute<{ results: Product[]; duration: number }>(
'search',
{ products, query: searchQuery }
);
setResults(results);
setSearchTime(duration);
}, [products, execute]);
// Debounced search
useEffect(() => {
const timer = setTimeout(() => {
handleSearch(query);
}, 300);
return () => clearTimeout(timer);
}, [query, handleSearch]);
return (
<div>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/>
<p>Found {results.length} products in {searchTime.toFixed(2)}ms</p>
<ul>
{results.map(product => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
</div>
);
}
Bildverarbeitung ist CPU-intensiv – perfekt für Workers.
// workers/image.worker.ts
interface ImageProcessRequest {
id: string;
type: 'grayscale' | 'blur' | 'brighten';
payload: {
imageData: ImageData;
intensity?: number;
};
}
self.addEventListener('message', (event: MessageEvent<ImageProcessRequest>) => {
const { id, type, payload } = event.data;
const { imageData, intensity = 1 } = payload;
const processed = processImage(imageData, type, intensity);
self.postMessage({ id, result: processed }, [processed.data.buffer]);
});
function processImage(
imageData: ImageData,
type: string,
intensity: number
): ImageData {
const data = imageData.data;
const result = new ImageData(
new Uint8ClampedArray(data),
imageData.width,
imageData.height
);
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
switch (type) {
case 'grayscale': {
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
result.data[i] = gray;
result.data[i + 1] = gray;
result.data[i + 2] = gray;
break;
}
case 'brighten': {
result.data[i] = Math.min(255, r * intensity);
result.data[i + 1] = Math.min(255, g * intensity);
result.data[i + 2] = Math.min(255, b * intensity);
break;
}
// ... andere Filter
}
result.data[i + 3] = data[i + 3]; // Alpha beibehalten
}
return result;
}// ImageEditor.tsx
import { useRef, useState } from 'react';
import { useWorker } from './hooks/useWorker';
function ImageEditor() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [processing, setProcessing] = useState(false);
const { execute } = useWorker({ workerUrl: './workers/image.worker.ts' });
const applyFilter = async (filter: 'grayscale' | 'blur' | 'brighten') => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
setProcessing(true);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
try {
const processed = await execute<ImageData>(filter, {
imageData,
intensity: 1.5
});
ctx.putImageData(processed, 0, 0);
} finally {
setProcessing(false);
}
};
return (
<div>
<canvas ref={canvasRef} width={800} height={600} />
<div>
<button onClick={() => applyFilter('grayscale')} disabled={processing}>
Grayscale
</button>
<button onClick={() => applyFilter('brighten')} disabled={processing}>
Brighten
</button>
</div>
{processing && <p>Processing image...</p>}
</div>
);
}
Wichtig: postMessage mit
Transferable Objects (zweiter Parameter) für große
Daten:
self.postMessage(
{ id, result: processed },
[processed.data.buffer] // Transfer ownership statt copy
);Transferable Objects werden zwischen Threads verschoben statt kopiert – massiver Performance-Gewinn bei großen Arrays.
Vite unterstützt Workers out-of-the-box mit spezieller Syntax:
// ✓ Vite-Syntax für Worker-Import
const worker = new Worker(
new URL('./worker.ts', import.meta.url),
{ type: 'module' } // ESM-Worker
);
Warum
new URL(..., import.meta.url)?
Vite braucht statische Imports zur Build-Zeit. new URL
mit import.meta.url ermöglicht es Vite, den Worker-Code zu
bundlen.
// ❌ Falsch: String-Pfad funktioniert nicht
const worker = new Worker('./worker.ts');
// ❌ Falsch: Variable Path funktioniert nicht
const workerPath = './worker.ts';
const worker = new Worker(workerPath);
// ✓ Richtig: URL-Constructor mit import.meta.url
const worker = new Worker(
new URL('./worker.ts', import.meta.url),
{ type: 'module' }
);
Für komplexere Setups:
npm install vite-plugin-worker// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import worker from 'vite-plugin-worker';
export default defineConfig({
plugins: [
react(),
worker()
],
worker: {
format: 'es', // ESM-Format
}
});Comlink macht Worker-Kommunikation einfacher – wie normale Function Calls.
npm install comlink// workers/api.worker.ts
import { expose } from 'comlink';
const api = {
async fibonacci(n: number): Promise<number> {
if (n <= 1) return n;
return (await fibonacci(n - 1)) + (await fibonacci(n - 2));
},
async searchProducts(products: Product[], query: string): Promise<Product[]> {
return products.filter(p =>
p.name.toLowerCase().includes(query.toLowerCase())
);
},
async processImage(imageData: ImageData, filter: string): Promise<ImageData> {
// ... processing logic
return processedImageData;
}
};
expose(api);
function fibonacci(n: number): number {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}// hooks/useComlinkWorker.ts
import { useEffect, useState } from 'react';
import { wrap, Remote } from 'comlink';
export function useComlinkWorker<T>(workerUrl: string) {
const [api, setApi] = useState<Remote<T> | null>(null);
useEffect(() => {
const worker = new Worker(new URL(workerUrl, import.meta.url), {
type: 'module'
});
const wrappedApi = wrap<T>(worker);
setApi(wrappedApi);
return () => worker.terminate();
}, [workerUrl]);
return api;
}
// App.tsx - Verwendung
import { useComlinkWorker } from './hooks/useComlinkWorker';
interface WorkerApi {
fibonacci(n: number): Promise<number>;
searchProducts(products: Product[], query: string): Promise<Product[]>;
}
function App() {
const [result, setResult] = useState<number>(0);
const api = useComlinkWorker<WorkerApi>('./workers/api.worker.ts');
const calculate = async () => {
if (!api) return;
// Sieht aus wie normaler Function Call!
const result = await api.fibonacci(45);
setResult(result);
};
return (
<div>
<button onClick={calculate}>Calculate</button>
<p>Result: {result}</p>
</div>
);
}
Comlink-Vorteile: - Keine Message-IDs managen - TypeScript-Types bleiben erhalten - Error-Handling automatisch - Cleaner Code
Workers haben keinen Zugriff auf:
| Feature | Verfügbar? | Grund |
|---|---|---|
| DOM | ❌ Nein | Kein document, window |
| React Components | ❌ Nein | Braucht DOM |
| LocalStorage | ❌ Nein | Synchronous API |
| Cookies | ❌ Nein | Security |
| Fetch API | ✅ Ja | Async, isolated |
| IndexedDB | ✅ Ja | Async, isolated |
| WebSockets | ✅ Ja | Async, isolated |
console.log |
✅ Ja | Logging OK |
setTimeout |
✅ Ja | Async OK |
crypto |
✅ Ja | Krypto-Funktionen OK |
// ❌ Falsch: DOM-Zugriff im Worker
self.addEventListener('message', () => {
const div = document.createElement('div'); // Error: document is not defined
const value = localStorage.getItem('key'); // Error: localStorage is not defined
});
// ✓ Richtig: Fetch, Crypto, Timer
self.addEventListener('message', async () => {
const response = await fetch('/api/data'); // OK
const data = await response.json();
const hash = await crypto.subtle.digest('SHA-256', data); // OK
setTimeout(() => {
self.postMessage('Done');
}, 1000); // OK
});Browser DevTools unterstützen Worker-Debugging:
Chrome DevTools: 1. Sources Tab → Worker-Threads links 2. Breakpoints in Worker-Code setzen 3. Console.log aus Worker erscheint in Main Console
// workers/debug.worker.ts
self.addEventListener('message', (event) => {
console.log('Worker received:', event.data); // Sichtbar in DevTools
debugger; // Breakpoint funktioniert!
const result = process(event.data);
console.log('Worker sending:', result);
self.postMessage(result);
});Performance-Profiling:
self.addEventListener('message', (event) => {
console.time('Worker processing');
const result = heavyComputation(event.data);
console.timeEnd('Worker processing');
self.postMessage(result);
});Fehler 1: Shared State zwischen Main und Worker
// ❌ Falsch: Objekte werden kopiert, nicht geteilt
const data = { count: 0 };
worker.postMessage(data);
data.count = 10; // Worker sieht das NICHT!
// Worker empfängt Copy mit count: 0Workers teilen keinen Memory. postMessage kopiert Daten
(Structured Clone).
Fehler 2: Funktionen in postMessage
// ❌ Falsch: Funktionen können nicht serialisiert werden
worker.postMessage({
data: [1, 2, 3],
callback: (result) => console.log(result) // Error!
});
// ✓ Richtig: Nur Daten, Logic im Worker
worker.postMessage({ data: [1, 2, 3] });
worker.onmessage = (event) => {
console.log(event.data); // Callback im Main Thread
};Fehler 3: Worker nicht terminieren
// ❌ Falsch: Worker läuft ewig
function Component() {
const worker = new Worker(...);
// Worker wird nie gestoppt!
return <div>...</div>;
}
// ✓ Richtig: Cleanup in useEffect
function Component() {
useEffect(() => {
const worker = new Worker(...);
return () => worker.terminate(); // Cleanup!
}, []);
}
Fehler 4: Synchrone Antwort erwarten
// ❌ Falsch: postMessage ist async!
worker.postMessage({ data: 42 });
const result = worker.result; // Gibt's nicht!
// ✓ Richtig: onmessage-Handler oder Promise
worker.postMessage({ data: 42 });
worker.onmessage = (event) => {
const result = event.data;
};
// Oder mit Promise-Wrapper (wie useWorker Hook)Fehler 5: Worker-Path relativ zu HTML
// ❌ Falsch: Relativ zu index.html (funktioniert nicht nach Build)
const worker = new Worker('/src/workers/compute.worker.ts');
// ✓ Richtig: import.meta.url für Build-Tool
const worker = new Worker(
new URL('./workers/compute.worker.ts', import.meta.url),
{ type: 'module' }
);Wann lohnt sich ein Worker?
| Task | Duration | Worker? | Grund |
|---|---|---|---|
| Array.sort() 100 items | <1ms | ❌ Nein | Overhead > Gewinn |
| JSON.parse() 1MB | ~10ms | ⚠️ Maybe | Grenzfall |
| Fibonacci(45) | ~5000ms | ✅ Ja | Blockiert UI lange |
| Image Processing | ~100ms+ | ✅ Ja | CPU-intensiv |
| Search 100k items | ~50ms+ | ✅ Ja | Hängt von UX ab |
Overhead einkalkulieren: - Worker-Startup: ~50-100ms - Message-Serialisierung: ~1-10ms - Context-Switch: ~1ms
Wenn Task <50ms dauert, ist Worker oft langsamer als direkter Code.
Benchmark-Pattern:
// Vergleiche mit und ohne Worker
function benchmark() {
// Ohne Worker
console.time('Main Thread');
const result1 = fibonacci(40);
console.timeEnd('Main Thread');
// Mit Worker
console.time('Worker Thread');
worker.postMessage(40);
worker.onmessage = () => {
console.timeEnd('Worker Thread');
};
}Für maximale Performance: WebAssembly im Worker.
// workers/wasm.worker.ts
self.addEventListener('message', async (event) => {
// WASM-Module laden
const response = await fetch('/compute.wasm');
const buffer = await response.arrayBuffer();
const module = await WebAssembly.instantiate(buffer);
// WASM-Function aufrufen
const result = module.instance.exports.fibonacci(event.data);
self.postMessage(result);
});WebAssembly ist 10-100x schneller als JavaScript für CPU-intensive Tasks – kombiniert mit Workers ideal für: - Videokodierung - Krypto-Berechnungen - Wissenschaftliche Simulationen - Game-Engines
Web Workers sind kein Nice-to-Have für Performance-kritische Apps – sie sind essentiell. Der Main Thread ist zu wertvoll, um ihn mit Berechnungen zu blockieren. Workers ermöglichen echte Parallelität im Browser – nutze sie für CPU-intensive Tasks, und die UI bleibt smooth.