JavaScript and TypeScript course Β· Module 10: TypeScript in Practice

Web Workers for Heavy Computations

28 min read
In this lesson7

Imagine that the park lab is sequencing T-Rex DNA while the control room freezes: the cameras stop and the alarm button does not respond. That is how a browser behaves when heavy computations land on the main thread. Web Workers are a technology that lets you run JavaScript in separate threads, independently of the main UI (User Interface) thread, so the interface stays responsive.

What you will learn:

  • move computations to a separate thread and talk to it through postMessage
  • recognize the limits of a worker and tell Dedicated, Shared and Service Workers apart
  • build a worker pool and share memory through SharedArrayBuffer
  • measure performance and handle worker failures

Why Are Web Workers Needed?

The Problem of JavaScript's Single-Threaded Nature

Your code runs on a single main thread, which also handles clicks, scrolling and painting the page. See what a long loop does to it:

1// Problem: Blocking computations on the main thread
2function heavyCalculation(n) {
3  console.log('Starting heavy computations...');
4
5  // Simulation of intensive computations
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('Computations completed');
14  return result;
15}
16
17// This will block the UI for several seconds!
18document.getElementById('calculate-button').addEventListener('click', () => {
19  const result = heavyCalculation(5000);
20  document.getElementById('result').textContent = result;
21});
22
23// During computations the user cannot:
24// - Click other buttons
25// - Scroll the page
26// - Use animations
27// - Type text in forms

As long as the computation runs, the event loop cannot handle a click or an animation frame. This is the problem Web Workers solve.

Performance Comparison

A worker does not compute faster, because it is the same JavaScript engine. The difference is who has to wait. This benchmark sketch compares both approaches:

1// Benchmark: Main Thread vs Web Worker
2const performanceTest = {
3  // Test without Web Worker - blocks 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 with Web Worker - does not block 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};

The time may be similar, but with a worker the interface keeps responding the whole time.

Basic Web Worker Implementations

Creating a Simple Web Worker

You create a worker with the new Worker('worker.js') constructor, which runs the script in a separate thread. The order matters: first set up the onmessage listener, then send data with postMessage, and finally end the work with the terminate() method:

1// main.js - main thread
2class WebWorkerManager {
3  constructor() {
4    this.worker = null;
5    this.setupWorker();
6  }
7
8  setupWorker() {
9    // Check Web Workers support
10    if (typeof Worker !== 'undefined') {
11      this.worker = new Worker('worker.js');
12
13      // Handle messages from the worker
14      this.worker.onmessage = (event) => {
15        this.handleWorkerMessage(event.data);
16      };
17
18      // Handle errors
19      this.worker.onerror = (error) => {
20        console.error('Worker error:', error);
21      };
22
23      console.log('Web Worker has been created');
24    } else {
25      console.warn('Web Workers are not supported in this browser');
26    }
27  }
28
29  // Send a task to the 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  // Handle responses from the 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  // Terminate the worker
56  terminateWorker() {
57    if (this.worker) {
58      this.worker.terminate();
59      this.worker = null;
60      console.log('Web Worker has been terminated');
61    }
62  }
63}
64
65// Usage
66const workerManager = new WebWorkerManager();
67
68// Send a computation task
69document.getElementById('start-calculation').addEventListener('click', () => {
70  workerManager.sendTask('CALCULATE_PRIMES', { max: 100000 });
71});

You read the response from event.data, by assigning a function to onmessage or through addEventListener('message', ...). The data is not shared: the browser copies it with the structured clone algorithm, so you cannot send functions or DOM elements.

Implementing worker.js

On the other side, the worker listens with self.onmessage, replies with self.postMessage and can report progress:

1// worker.js - code executed in a separate thread
2class WorkerCalculations {
3  constructor() {
4    this.setupMessageHandler();
5  }
6
7  setupMessageHandler() {
8    // Main message handler from the main thread
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  // Calculate prime numbers with progress reporting
40  calculatePrimes(max) {
41    const primes = [];
42    const updateInterval = Math.floor(max / 100); // Update every 1%
43
44    for (let i = 2; i <= max; i++) {
45      if (this.isPrime(i)) {
46        primes.push(i);
47      }
48
49      // Report progress
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  // Image processing (filtering, 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  // Sorting large arrays
104  sortArray(array) {
105    const startTime = performance.now();
106
107    // Merge sort implementation for better performance
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  // Calculate Pi using Monte Carlo method
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 to 1
154      const y = Math.random() * 2 - 1; // -1 to 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  // Helper methods for 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// Initialize worker
205new WorkerCalculations();

A worker has its own global scope (self) and no access to the DOM, document or window. It can use fetch, XMLHttpRequest, setTimeout, setInterval and IndexedDB, though. So the lifecycle looks like this: creation, postMessage(), work in the worker thread and receiving the result in onmessage.

Advanced Web Worker Patterns

Worker Pool for Parallel Computations

A pool creates several workers, usually as many as the number of cores reported by navigator.hardwareConcurrency, and distributes tasks from a queue:

1// WorkerPool.js - Managing multiple 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  // Execute a task using the 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      // Pass progress update (optional)
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    // Check if there are tasks in the queue
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  // Parallel processing of large datasets
109  async processInParallel(dataArray, chunkProcessor) {
110    const chunkSize = Math.ceil(dataArray.length / this.poolSize);
111    const chunks = [];
112
113    // Split data into chunks
114    for (let i = 0; i < dataArray.length; i += chunkSize) {
115      chunks.push(dataArray.slice(i, i + chunkSize));
116    }
117
118    // Process chunks in parallel
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    // Combine results
130    return results.flat();
131  }
132
133  generateTaskId() {
134    return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
135  }
136
137  // Terminate all 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  // Pool statistics
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// Using Worker Pool
160const workerPool = new WorkerPool('calculation-worker.js', 8);
161
162// Example of parallel processing
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      // Processing function will be executed in a worker
169      return chunk.map(x => Math.sqrt(x * x + 1));
170    });
171
172    console.log('Processing completed:', results.length);
173    console.log('Stats:', workerPool.getStats());
174  } catch (error) {
175    console.error('Processing error:', error);
176  }
177}

When all workers are busy, a task waits in the queue, and releaseWorker hands out the next one right away. chunkProcessor reaches the worker as text, because postMessage cannot carry functions.

Shared Array Buffer for Efficient Communication

SharedArrayBuffer is memory visible to several threads at once, so the workers write the results of matrix multiplication straight into a shared buffer, without copying:

1// SharedArrayBufferWorker.js - Shared memory between workers
2class SharedArrayBufferManager {
3  constructor() {
4    this.checkSupport();
5  }
6
7  checkSupport() {
8    if (typeof SharedArrayBuffer === 'undefined') {
9      console.warn('SharedArrayBuffer is not supported');
10      this.fallbackToTransferableObjects();
11      return false;
12    }
13    return true;
14  }
15
16  // Create shared memory for matrix computations
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      // Helper methods (they use the matrix variable, because this in an arrow function is the manager)
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  // Parallel matrix multiplication
44  async parallelMatrixMultiply(matrixA, matrixB, workerCount = 4) {
45    const rowsA = matrixA.rows;
46    const colsA = matrixA.cols;
47    const colsB = matrixB.cols;
48
49    // Create shared result matrix
50    const resultMatrix = this.createSharedMatrixBuffer(rowsA, colsB);
51
52    // Distribute work among 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('Using Transferable Objects as an alternative');
97    // Implementation with Transferable Objects
98  }
99}
100
101// matrix-worker.js - Worker for matrix computations
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    // Matrix multiplication for assigned rows
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};

Each worker computes different rows, so they do not get in each other's way. When threads change the same cells, use Atomics (for example Atomics.add and Atomics.wait), which operate atomically. Browsers expose SharedArrayBuffer only to pages isolated with the Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp headers.

Service Worker for Background Processing

Besides the regular (dedicated) worker there are two more kinds. A Shared Worker, created with new SharedWorker(), is shared by tabs and windows from the same origin. A Service Worker sits between the page and the network and handles caching, offline mode and background sync:

1// ServiceWorkerManager.js - Managing long-running background tasks
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        // Wait for service worker activation
15        await navigator.serviceWorker.ready;
16
17        this.serviceWorker = registration.active || registration.waiting || registration.installing;
18        this.setupMessageChannel();
19
20        console.log('Service Worker registered and ready');
21      } catch (error) {
22        console.error('Service Worker registration error:', error);
23      }
24    }
25  }
26
27  setupMessageChannel() {
28    this.messageChannel = new MessageChannel();
29
30    // Handle messages from the Service Worker
31    this.messageChannel.port1.onmessage = (event) => {
32      this.handleServiceWorkerMessage(event.data);
33    };
34
35    // Send port to the Service Worker
36    navigator.serviceWorker.controller?.postMessage(
37      { type: 'INIT_PORT' },
38      [this.messageChannel.port2]
39    );
40  }
41
42  // Schedule a long-running task for the 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  // Example: Periodic data synchronization
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  // Example: Background file processing
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(`Task ${task.id} completed:`, result);
94
95    // Update UI or application state
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      // Store in IndexedDB or 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    // File processing simulation
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    // IndexedDB storage implementation
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();

A Service Worker is not suited to long computations, though, because the browser puts it to sleep when it is not handling events. On the first visit navigator.serviceWorker.controller can be null, hence the ?. operator in the code.

Performance Monitoring and Debugging

The monitor wraps postMessage and onmessage to record the duration of every task:

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  // Monitor worker performance
15  monitorWorkerPerformance(worker, taskId) {
16    const startTime = performance.now();
17    let memoryBefore = null;
18
19    // Memory measurement (if available)
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      // Record execution time metric
39      this.metrics.taskExecutionTimes.push({
40        taskId: taskId,
41        executionTime: executionTime,
42        timestamp: Date.now()
43      });
44
45      // Memory measurement after execution
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  // Performance analysis
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 seconds
126      recommendations.push('Consider splitting long-running tasks into smaller chunks');
127    }
128
129    if (memoryTrends?.potentialMemoryLeak) {
130      recommendations.push('Potential memory leak detected - review memory management');
131    }
132
133    if (this.metrics.taskExecutionTimes.length > 1000) {
134      recommendations.push('Large number of tasks - consider implementing a Worker Pool');
135    }
136
137    return recommendations;
138  }
139}
140
141// Monitoring usage
142const performanceMonitor = new WorkerPerformanceMonitor();
143
144// Monitor a specific worker
145const worker = new Worker('calculation-worker.js');
146const monitoredWorker = performanceMonitor.monitorWorkerPerformance(worker, 'task_001');
147
148// Generate report after some time
149setTimeout(() => {
150  const report = performanceMonitor.generatePerformanceReport();
151  console.log('Performance Report:', report);
152}, 30000);

performance.memory is a non-standard API available only in Chromium, so treat these measurements as approximate.

Best Practices and Optimizations

Optimal Communication with Workers

Every message has a copying cost, so we batch small ones, compress large ones and pass buffers without copying:

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  // Message batching for better performance
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 after timeout if batch is not full
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    // Group messages by workers
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    // Send batched messages
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  // Compress large data before sending
50  async sendCompressedData(worker, data) {
51    const serializedData = JSON.stringify(data);
52
53    if (serializedData.length > this.compressionThreshold) {
54      try {
55        // Use Compression Streams API (if available)
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 to uncompressed data
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    // Compression implementation (example with 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 - return original data
106    return new TextEncoder().encode(data);
107  }
108
109  // Transferable Objects for efficient transfer
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]) passes the buffer as a transferable object (Transferable): instead of a copy, the ownership changes, and on the main thread the buffer then has a length of 0.

Error Handling and Resilience

A worker can die because of an error in the script. A resilient manager retries tasks with a growing delay (exponential backoff), enforces time limits and recreates dead workers:

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  // Create a worker with 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 with 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  // Execute a task with 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 on success
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 if needed
74          if (error.type === 'WORKER_DIED') {
75            await this.recreateWorker(workerId);
76          }
77        }
78      }
79    }
80
81    // Record task as 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      // Record task as 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    // Reject all active tasks
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 for the new 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  // Periodic worker health checking
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      // Check if worker is responsive
219      if (timeSinceLastActivity > this.options.healthCheckInterval * 2) {
220        console.warn(`Worker ${workerId} appears unresponsive`);
221        this.recreateWorker(workerId);
222      }
223
224      // Check 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    // Select the worker with the fewest active tasks
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  // Resource cleanup
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  // System statistics
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}

The time limit of each task protects you from a frozen worker, and PING and PONG check whether a new worker responds.

Practical example: 3D rendering

Finally, the renderer splits the screen into 64 by 64 pixel tiles and hands them to a worker pool, which computes the colors with 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    // Add 3D objects to the scene
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    // Split screen into tiles for parallel rendering
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    // Wait for all tiles
64    const renderedTiles = await Promise.all(renderPromises);
65
66    // Compose tiles on 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  // Scene animation
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        // Transform screen coordinates to 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 }; // Background
164
165    // Check collision with objects
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    // Simplified lighting
213    const baseColor = obj.type === 'sphere'
214      ? { r: 255, g: 100, b: 100 }
215      : { r: 100, g: 255, b: 100 };
216
217    // Lighting simulation
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();

The main thread only assembles the tiles on the canvas. It would be faster to pass the pixel buffers as Transferables instead of converting them to arrays with Array.from.

Summary

Web Workers enable:

  1. Running heavy computations without blocking the UI
  2. Parallel processing of large datasets
  3. Better responsiveness of web applications
  4. Scalability through a Worker Pool
  5. Background processing with Service Workers

You will reach for them in mathematical and scientific computations, image and video processing, 3D rendering, sorting and filtering large datasets, compression, cryptography and hashing, and running machine learning models. Start with a single worker in module mode (new Worker('worker.js', { type: 'module' })), and build a pool only when measurements show that it is not enough. A worker simulation waits in the editor below, and in the next lesson we will look into memory and the garbage collector.

Remember: a worker is a separate lab behind glass that works in parallel, but you talk to it only through the postMessage window.

Code for this lesson: index.js
1// Web Workers - prime number calculation example
2// Main thread
3console.log("Jurassic Park - DNA Analytics System");
4console.log("Starting heavy calculations without blocking UI\n");
5
6// Simulating a Web Worker (a real worker requires a separate file)
7function simulateWorker() {
8  // Function checking if a number is prime
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  // Find prime numbers in range
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// Main function
35async function calculatePrimesInBackground() {
36  console.log("Starting calculations in the background...");
37  console.log("The UI stays responsive!\n");
38
39  const findPrimes = simulateWorker();
40
41  // Simulation of long-running computations
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(`\nFound ${primes.length} prime numbers`);
50  console.log(`First 20: ${primes.slice(0, 20).join(', ')}`);
51  console.log(`Last 20: ${primes.slice(-20).join(', ')}`);
52}
53
54// Worker API structure (for reference)
55console.log("=== Web Worker API Structure ===");
56console.log("1. Creating a worker:");
57console.log("   const worker = new Worker('worker.js');");
58console.log("\n2. Sending data to the worker:");
59console.log("   worker.postMessage({ action: 'calculate', data: 100 });");
60console.log("\n3. Receiving results:");
61console.log("   worker.onmessage = (event) => console.log(event.data);");
62console.log("\n4. Error handling:");
63console.log("   worker.onerror = (error) => console.error(error);");
64console.log("\n5. Closing the worker:");
65console.log("   worker.terminate();\n");
66
67// Run the calculations
68calculatePrimesInBackground();
69
70// Demonstration of concurrent UI operation
71console.log("\nUI works normally during calculations!");
72console.log("Other operations can be performed...");
73console.log("Clicks, animations, everything is responsive!");
74
75// Example of a "worker" using a 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 with a promise-based worker
102const workerSim = new PrimeWorkerSimulator();
103workerSim.calculate(10000, 11000).then(primes => {
104  console.log(`\nComputation completed! Found ${primes.length} prime numbers between 10000 and 11000`);
105});

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is the main problem that Web Workers solve?

  2. 2. Which method do we use to send data to a Web Worker?

These are 2 of 5 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Create a worker for calculating prime numbers.

  • Click in order

    Arrange the steps of communication with a Web Worker.

  • Horizontal ordering

    Arrange the elements for creating a Web Worker:

  • Click in order

    Arrange the elements of creating a Web Worker in the correct order:

  • Code editor

    Use SharedArrayBuffer for communication between threads.

  • Vertical ordering

    Arrange the Web Worker lifecycle stages in the correct order:

  • Click in order

    Arrange the elements of sending a message to a Worker:

Useful articles