Kurs JavaScript i TypeScript · Moduł 10: TypeScript w praktyce
Web Workers dla ciężkich obliczeń
W tej lekcji7
Wyobraź sobie że laboratorium parku sekwencjonuje DNA T-Rexa, a sterownia w tym czasie zamarza: kamery stoją, a przycisk alarmu nie reaguje. Tak zachowuje się przeglądarka, gdy ciężkie obliczenia trafią do głównego wątku. Web Workers to technologia umożliwiająca wykonywanie JavaScript w osobnych wątkach, niezależnie od głównego wątku UI (User Interface), dzięki czemu interfejs pozostaje responsywny.
Czego się nauczysz:
- przenosić obliczenia do osobnego wątku i rozmawiać z nim przez
postMessage - rozpoznawać ograniczenia workera i odróżniać Dedicated, Shared i Service Worker
- budować pulę workerów i współdzielić pamięć przez
SharedArrayBuffer - mierzyć wydajność i obsługiwać awarie workerów
Dlaczego Web Workers są potrzebne?
Problem jednowątkowej natury JavaScript
Twój kod działa w jednym wątku głównym, który obsługuje też kliknięcia, przewijanie i rysowanie strony. Zobacz, co robi z nim długa pętla:
1// Problem: Blokujące obliczenia w głównym wątku
2function heavyCalculation(n) {
3 console.log('Rozpoczynanie ciężkich obliczeń...');
4
5 // Symulacja intensywnych obliczeń
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('Obliczenia zakończone');
14 return result;
15}
16
17// To zablokuje UI na kilka sekund!
18document.getElementById('calculate-button').addEventListener('click', () => {
19 const result = heavyCalculation(5000);
20 document.getElementById('result').textContent = result;
21});
22
23// Podczas obliczeń użytkownik nie może:
24// - Kliknąć innych przycisków
25// - Przewijać strony
26// - Używać animacji
27// - Wpisywać tekst w formularzeDopóki obliczenia trwają, pętla zdarzeń (event loop) nie obsłuży kliknięcia ani klatki animacji. Ten problem rozwiązują Web Workers.
Porównanie wydajności
Worker nie liczy szybciej, bo to ten sam silnik JavaScript. Różnica polega na tym, kto czeka. Szkic benchmarku porównuje oba podejścia:
1// Benchmark: Main Thread vs Web Worker
2const performanceTest = {
3 // Test bez Web Worker - blokuje 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 z Web Worker - nie blokuje 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};Czas bywa podobny, ale z workerem interfejs cały czas reaguje.
Podstawowe implementacje Web Workers
Tworzenie prostego Web Worker
Worker tworzysz konstruktorem new Worker('worker.js'), który uruchamia skrypt w osobnym wątku. Kolejność ma znaczenie: najpierw ustaw nasłuch onmessage, potem wysyłaj dane przez postMessage, a na końcu zakończ pracę metodą terminate():
1// main.js - główny wątek
2class WebWorkerManager {
3 constructor() {
4 this.worker = null;
5 this.setupWorker();
6 }
7
8 setupWorker() {
9 // Sprawdzenie wsparcia dla Web Workers
10 if (typeof Worker !== 'undefined') {
11 this.worker = new Worker('worker.js');
12
13 // Obsługa wiadomości z worker
14 this.worker.onmessage = (event) => {
15 this.handleWorkerMessage(event.data);
16 };
17
18 // Obsługa błędów
19 this.worker.onerror = (error) => {
20 console.error('Worker error:', error);
21 };
22
23 console.log('Web Worker został utworzony');
24 } else {
25 console.warn('Web Workers nie są wspierane w tej przeglądarce');
26 }
27 }
28
29 // Wysyłanie zadania do 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 // Obsługa odpowiedzi z 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 // Zakończenie pracy worker
56 terminateWorker() {
57 if (this.worker) {
58 this.worker.terminate();
59 this.worker = null;
60 console.log('Web Worker został zakończony');
61 }
62 }
63}
64
65// Użycie
66const workerManager = new WebWorkerManager();
67
68// Wysłanie zadania obliczeń
69document.getElementById('start-calculation').addEventListener('click', () => {
70 workerManager.sendTask('CALCULATE_PRIMES', { max: 100000 });
71});Odpowiedź czytasz z event.data, przypisując funkcję do onmessage albo przez addEventListener('message', ...). Dane nie są współdzielone: przeglądarka kopiuje je algorytmem structured clone, więc funkcji ani elementów DOM nie wyślesz.
Implementacja worker.js
Po drugiej stronie worker nasłuchuje przez self.onmessage, odpowiada przez self.postMessage i może raportować postęp:
1// worker.js - kod wykonywany w osobnym wątku
2class WorkerCalculations {
3 constructor() {
4 this.setupMessageHandler();
5 }
6
7 setupMessageHandler() {
8 // Główny handler wiadomości z głównego wątku
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 // Obliczanie liczb pierwszych z raportowaniem postępu
40 calculatePrimes(max) {
41 const primes = [];
42 const updateInterval = Math.floor(max / 100); // Update co 1%
43
44 for (let i = 2; i <= max; i++) {
45 if (this.isPrime(i)) {
46 primes.push(i);
47 }
48
49 // Raportowanie postępu
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 // Przetwarzanie obrazu (filtrowanie, transformacje)
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 // Sortowanie dużych tablic
104 sortArray(array) {
105 const startTime = performance.now();
106
107 // Implementacja merge sort dla lepszej wydajności
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 // Obliczanie Pi metodą 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; // -1 do 1
154 const y = Math.random() * 2 - 1; // -1 do 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 // Metody pomocnicze do komunikacji
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// Inicjalizacja worker
205new WorkerCalculations();Worker ma własny zakres globalny (self) i nie ma dostępu do DOM, document ani window. Może za to używać fetch, XMLHttpRequest, setTimeout, setInterval i IndexedDB. Cykl życia wygląda więc tak: utworzenie, postMessage(), praca w wątku workera i odbiór wyniku w onmessage.
Zaawansowane wzorce Web Workers
Worker Pool dla równoległych obliczeń
Pula (pool) tworzy kilka workerów, zwykle tyle, ile rdzeni podaje navigator.hardwareConcurrency, i rozdziela zadania z kolejki:
1// WorkerPool.js - Zarządzanie wieloma worker'ami
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 // Wykonanie zadania z wykorzystaniem 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 // Przekazanie aktualizacji postępu (opcjonalne)
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 // Sprawdzenie czy są zadania w kolejce
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 // Równoległe przetwarzanie dużych zbiorów danych
109 async processInParallel(dataArray, chunkProcessor) {
110 const chunkSize = Math.ceil(dataArray.length / this.poolSize);
111 const chunks = [];
112
113 // Podział danych na chunks
114 for (let i = 0; i < dataArray.length; i += chunkSize) {
115 chunks.push(dataArray.slice(i, i + chunkSize));
116 }
117
118 // Równoległe przetwarzanie 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 // Połączenie wyników
130 return results.flat();
131 }
132
133 generateTaskId() {
134 return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
135 }
136
137 // Zamknięcie wszystkich worker'ów
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 // Statystyki 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// Użycie Worker Pool
160const workerPool = new WorkerPool('calculation-worker.js', 8);
161
162// Przykład równoległego przetwarzania
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 // Funkcja przetwarzania będzie wykonana w worker
169 return chunk.map(x => Math.sqrt(x * x + 1));
170 });
171
172 console.log('Przetwarzanie zakończone:', results.length);
173 console.log('Stats:', workerPool.getStats());
174 } catch (error) {
175 console.error('Błąd przetwarzania:', error);
176 }
177}Gdy wszystkie workery są zajęte, zadanie czeka w kolejce, a releaseWorker od razu podaje następne. chunkProcessor trafia do workera jako tekst, bo postMessage nie przenosi funkcji.
Shared Array Buffer dla wydajnej komunikacji
SharedArrayBuffer to pamięć widoczna dla kilku wątków naraz, więc workery zapisują wyniki mnożenia macierzy prosto do wspólnego bufora, bez kopiowania:
1// SharedArrayBufferWorker.js - Współdzielona pamięć między worker'ami
2class SharedArrayBufferManager {
3 constructor() {
4 this.checkSupport();
5 }
6
7 checkSupport() {
8 if (typeof SharedArrayBuffer === 'undefined') {
9 console.warn('SharedArrayBuffer nie jest wspierany');
10 this.fallbackToTransferableObjects();
11 return false;
12 }
13 return true;
14 }
15
16 // Tworzenie współdzielonej pamięci do obliczeń macierzy
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 // Metody pomocnicze (używają zmiennej matrix, bo this w funkcji strzałkowej to menedżer)
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 // Równoległe mnożenie macierzy
44 async parallelMatrixMultiply(matrixA, matrixB, workerCount = 4) {
45 const rowsA = matrixA.rows;
46 const colsA = matrixA.cols;
47 const colsB = matrixB.cols;
48
49 // Tworzenie współdzielonej macierzy wynikowej
50 const resultMatrix = this.createSharedMatrixBuffer(rowsA, colsB);
51
52 // Podział pracy między worker'y
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('Używanie Transferable Objects jako alternatywy');
97 // Implementacja z Transferable Objects
98 }
99}
100
101// matrix-worker.js - Worker do obliczeń macierzy
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 // Mnożenie macierzy dla przydzielonych wierszy
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};Każdy worker liczy inne wiersze, więc nie wchodzą sobie w drogę. Gdy wątki zmieniają te same komórki, używaj Atomics (na przykład Atomics.add i Atomics.wait), które działają niepodzielnie. Przeglądarki udostępniają SharedArrayBuffer tylko stronom izolowanym nagłówkami Cross-Origin-Opener-Policy: same-origin i Cross-Origin-Embedder-Policy: require-corp.
Service Worker do Background Processing
Poza zwykłym (dedicated) workerem są jeszcze dwa rodzaje. Shared Worker, tworzony przez new SharedWorker(), jest współdzielony przez karty i okna z tego samego źródła (origin). Service Worker stoi między stroną a siecią i obsługuje cache, tryb offline oraz synchronizację w tle:
1// ServiceWorkerManager.js - Zarządzanie długotrwałymi zadaniami w tle
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 // Oczekiwanie na aktywację 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 zarejestrowany i gotowy');
21 } catch (error) {
22 console.error('Błąd rejestracji Service Worker:', error);
23 }
24 }
25 }
26
27 setupMessageChannel() {
28 this.messageChannel = new MessageChannel();
29
30 // Obsługa wiadomości od Service Worker
31 this.messageChannel.port1.onmessage = (event) => {
32 this.handleServiceWorkerMessage(event.data);
33 };
34
35 // Wysłanie portu do Service Worker
36 navigator.serviceWorker.controller?.postMessage(
37 { type: 'INIT_PORT' },
38 [this.messageChannel.port2]
39 );
40 }
41
42 // Zlecenie długotrwałego zadania do 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 // Przykład: Okresowe synchronizowanie danych
57 scheduleDataSync(interval = 300000) { // 5 minut
58 this.scheduleBackgroundTask({
59 type: 'DATA_SYNC',
60 interval: interval,
61 endpoint: '/api/sync',
62 recurring: true
63 });
64 }
65
66 // Przykład: Przetwarzanie plików w tle
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(`Zadanie ${task.id} zakończone:`, result);
94
95 // Aktualizacja UI lub stanu aplikacji
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 // Zapisanie w IndexedDB lub 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 // Symulacja przetwarzania pliku
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 // Implementacja zapisu w 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();Do długich obliczeń Service Worker się nie nadaje, bo przeglądarka usypia go, gdy nie obsługuje zdarzeń. Przy pierwszej wizycie navigator.serviceWorker.controller bywa null, stąd operator ?. w kodzie.
Performance monitoring i debugging
Monitor podmienia postMessage i onmessage, aby zapisać czas każdego zadania:
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 // Monitorowanie wydajności worker'a
15 monitorWorkerPerformance(worker, taskId) {
16 const startTime = performance.now();
17 let memoryBefore = null;
18
19 // Pomiar pamięci (jeśli dostępny)
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 // Zapisanie metryki czasu wykonania
39 this.metrics.taskExecutionTimes.push({
40 taskId: taskId,
41 executionTime: executionTime,
42 timestamp: Date.now()
43 });
44
45 // Pomiar pamięci po wykonaniu
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 // Analiza wydajności
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 minuta
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 sekund
126 recommendations.push('Rozważ podział długotrwałych zadań na mniejsze części');
127 }
128
129 if (memoryTrends?.potentialMemoryLeak) {
130 recommendations.push('Wykryto potencjalny memory leak - sprawdź zarządzanie pamięcią');
131 }
132
133 if (this.metrics.taskExecutionTimes.length > 1000) {
134 recommendations.push('Duża liczba zadań - rozważ implementację Worker Pool');
135 }
136
137 return recommendations;
138 }
139}
140
141// Użycie monitoring
142const performanceMonitor = new WorkerPerformanceMonitor();
143
144// Monitorowanie konkretnego worker'a
145const worker = new Worker('calculation-worker.js');
146const monitoredWorker = performanceMonitor.monitorWorkerPerformance(worker, 'task_001');
147
148// Generowanie raportu po pewnym czasie
149setTimeout(() => {
150 const report = performanceMonitor.generatePerformanceReport();
151 console.log('Performance Report:', report);
152}, 30000);performance.memory to niestandardowe API dostępne tylko w Chromium, więc traktuj te pomiary jako orientacyjne.
Best practices i optymalizacje
Optymalna komunikacja z worker'ami
Każda wiadomość to koszt kopiowania, więc małe łączymy w paczki, duże kompresujemy, a bufory przekazujemy bez kopiowania:
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 wiadomości dla lepszej wydajności
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 po timeout jeśli batch nie jest pełny
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 // Grupowanie wiadomości po worker'ach
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 // Wysyłanie zbatchowanych wiadomości
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 // Kompresja dużych danych przed wysłaniem
50 async sendCompressedData(worker, data) {
51 const serializedData = JSON.stringify(data);
52
53 if (serializedData.length > this.compressionThreshold) {
54 try {
55 // Używanie Compression Streams API (jeśli dostępne)
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 // Fallback do niezkompresowanych danych
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 // Implementacja kompresji (przykład z 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 // Fallback - zwrócenie oryginalnych danych
106 return new TextEncoder().encode(data);
107 }
108
109 // Transferable Objects dla efektywnego transferu
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]) przekazuje bufor jako obiekt transferowalny (Transferable): zamiast kopii zmienia się właściciel, a w wątku głównym bufor ma potem długość 0.
Error handling i resilience
Worker może paść przez błąd w skrypcie. Odporny menedżer ponawia zadania z rosnącym opóźnieniem (exponential backoff), pilnuje limitów czasu i odtwarza martwe workery:
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 // Tworzenie worker'a z error handling
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 // Message handling z 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 // Wykonanie zadania z retry logic
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 // Reset failed task counter po sukcesie
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 // Recreate worker jeśli potrzeba
74 if (error.type === 'WORKER_DIED') {
75 await this.recreateWorker(workerId);
76 }
77 }
78 }
79 }
80
81 // Zapisanie zadania jako failed
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 // Zapisanie zadania jako aktywne
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 // Odrzucenie wszystkich aktywnych zadań
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 dla nowego worker'a
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 // Okresowe sprawdzanie zdrowia worker'ów
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 // Sprawdzenie czy worker jest responsywny
219 if (timeSinceLastActivity > this.options.healthCheckInterval * 2) {
220 console.warn(`Worker ${workerId} appears unresponsive`);
221 this.recreateWorker(workerId);
222 }
223
224 // Sprawdzenie error rate
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 // Wybór worker'a z najmniejszą liczbą aktywnych zadań
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 // Czyszczenie zasobów
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 // Statystyki systemu
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}Limit czasu każdego zadania chroni przed zawieszonym workerem, a PING i PONG sprawdzają, czy nowy worker odpowiada.
Praktyczny przykład: renderowanie 3D
Na koniec renderer dzieli ekran na kafelki 64 na 64 piksele i oddaje je puli workerów, które liczą kolory metodą śledzenia promieni (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 // Dodanie obiektów 3D do sceny
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 // Podział ekranu na tiles dla równoległego renderowania
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 // Oczekiwanie na wszystkie tiles
64 const renderedTiles = await Promise.all(renderPromises);
65
66 // Składanie tiles na 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 // Animacja sceny
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 // Przekształcenie współrzędnych ekranu na ray
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 }; // Tło
164
165 // Sprawdzanie kolizji z obiektami
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 // Uproszczone oświetlenie
213 const baseColor = obj.type === 'sphere'
214 ? { r: 255, g: 100, b: 100 }
215 : { r: 100, g: 255, b: 100 };
216
217 // Symulacja oświetlenia
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();Wątek główny tylko składa kafelki na canvasie. Szybciej byłoby przekazywać bufory pikseli jako Transferable zamiast zamieniać je na tablice przez Array.from.
Podsumowanie
Web Workers umożliwiają:
- Wykonywanie ciężkich obliczeń bez blokowania UI
- Równoległe przetwarzanie dużych zbiorów danych
- Lepszą responsywność aplikacji webowych
- Skalowalność poprzez Worker Pool
- Background processing z Service Workers
Sięgniesz po nie przy obliczeniach matematycznych i naukowych, przetwarzaniu obrazów i wideo, renderowaniu 3D, sortowaniu i filtrowaniu dużych zbiorów danych, kompresji, kryptografii i haszowaniu oraz uruchamianiu modeli uczenia maszynowego. Zacznij od jednego workera w trybie modułu (new Worker('worker.js', { type: 'module' })), a pulę buduj dopiero, gdy pomiary pokażą, że to za mało. W edytorze poniżej czeka symulacja workera, a w kolejnej lekcji zajrzymy do pamięci i garbage collectora.
Pamiętaj: worker to osobne laboratorium za szybą, które pracuje równolegle, ale rozmawiasz z nim tylko przez okienko postMessage.
Kod do tej lekcji: index.js
1// Web Workers - przykład obliczania liczb pierwszych
2// Główny wątek
3console.log("Park Jurajski - System Analityczny DNA");
4console.log("Rozpoczynanie ciężkich obliczeń bez blokowania UI\n");
5
6// Symulacja Web Worker (prawdziwy worker wymaga osobnego pliku)
7function simulateWorker() {
8 // Funkcja sprawdzająca czy liczba jest pierwsza
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 // Znajdź liczby pierwsze w zakresie
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// Główna funkcja
35async function calculatePrimesInBackground() {
36 console.log("Rozpoczynam obliczenia w tle...");
37 console.log("UI pozostaje responsywne!\n");
38
39 const findPrimes = simulateWorker();
40
41 // Symulacja długotrwałych obliczeń
42 const start = 1;
43 const end = 10000;
44
45 console.time("Czas obliczeń");
46 const primes = findPrimes(start, end);
47 console.timeEnd("Czas obliczeń");
48
49 console.log(`\nZnaleziono ${primes.length} liczb pierwszych`);
50 console.log(`Pierwsze 20: ${primes.slice(0, 20).join(', ')}`);
51 console.log(`Ostatnie 20: ${primes.slice(-20).join(', ')}`);
52}
53
54// Worker API structure (dla referencji)
55console.log("=== Struktura Web Worker API ===");
56console.log("1. Tworzenie workera:");
57console.log(" const worker = new Worker('worker.js');");
58console.log("\n2. Wysyłanie danych do workera:");
59console.log(" worker.postMessage({ action: 'calculate', data: 100 });");
60console.log("\n3. Odbieranie wyników:");
61console.log(" worker.onmessage = (event) => console.log(event.data);");
62console.log("\n4. Obsługa błędów:");
63console.log(" worker.onerror = (error) => console.error(error);");
64console.log("\n5. Zamykanie workera:");
65console.log(" worker.terminate();\n");
66
67// Uruchom obliczenia
68calculatePrimesInBackground();
69
70// Demonstracja równoczesnego działania UI
71console.log("\nUI działa normalnie podczas obliczeń!");
72console.log("Można wykonywać inne operacje...");
73console.log("Kliknięcia, animacje, wszystko jest responsywne!");
74
75// Przykład "workera" z promise
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 z promise-based worker
102const workerSim = new PrimeWorkerSimulator();
103workerSim.calculate(10000, 11000).then(primes => {
104 console.log(`\nObliczenia zakończone! Znaleziono ${primes.length} liczb pierwszych między 10000 a 11000`);
105});Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jaki jest główny problem, który rozwiązują Web Workers?
2. Jaką metodę używamy do wysłania danych do Web Workera?
To 2 z 5 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Stwórz worker do obliczania liczb pierwszych.
- Klikanie w kolejności
Uporządkuj kroki komunikacji z Web Worker.
- Układanie w poziomie
Ułóż elementy tworzenia Web Workera:
- Klikanie w kolejności
Ułóż elementy tworzenia Web Workera w odpowiedniej kolejności:
- Edytor kodu
Użyj SharedArrayBuffer do komunikacji między wątkami.
- Układanie w pionie
Ułóż etapy cyklu życia Web Workera w odpowiedniej kolejności:
- Klikanie w kolejności
Ułóż elementy wysyłania wiadomości do Workera: