JavaScript Web Workers API Complete Guide

Master multi-threading: Dedicated Workers, Shared Workers, message passing, and performance optimization

JavaScript Web Workers API Complete Guide

Web Workers allow running JavaScript in background threads. This article covers their usage and best practices.

Basic Concepts

Creating Workers

// Main thread
// Create Worker from external file
const worker = new Worker('worker.js');

// Create Worker from Blob
const code = `
  self.onmessage = function(e) {
    const result = e.data * 2;
    self.postMessage(result);
  };
`;
const blob = new Blob([code], { type: 'application/javascript' });
const worker2 = new Worker(URL.createObjectURL(blob));

// Module Worker (ES modules)
const moduleWorker = new Worker('worker.mjs', { type: 'module' });

Message Passing

// Main thread
const worker = new Worker('worker.js');

// Send message
worker.postMessage({ action: 'compute', data: [1, 2, 3, 4, 5] });

// Receive message
worker.onmessage = (event) => {
  console.log('Worker returned:', event.data);
};

// Error handling
worker.onerror = (error) => {
  console.error('Worker error:', error.message);
  console.error('File:', error.filename);
  console.error('Line:', error.lineno);
};

// Terminate Worker
worker.terminate();
// worker.js
self.onmessage = (event) => {
  const { action, data } = event.data;
  
  if (action === 'compute') {
    const result = data.reduce((sum, n) => sum + n, 0);
    self.postMessage({ result });
  }
};

// Worker closes itself
self.close();

Transferable Objects

// Main thread - Transfer ArrayBuffer (zero-copy)
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
const uint8 = new Uint8Array(buffer);

// Fill data
for (let i = 0; i < uint8.length; i++) {
  uint8[i] = i % 256;
}

// Transfer instead of copy (buffer becomes unusable in main thread)
worker.postMessage({ buffer }, [buffer]);

console.log(buffer.byteLength); // 0 - transferred

// worker.js
self.onmessage = (event) => {
  const { buffer } = event.data;
  const uint8 = new Uint8Array(buffer);
  
  // Process data...
  
  // Transfer back to main thread
  self.postMessage({ buffer }, [buffer]);
};

Advanced Usage

Worker Wrapper Class

class WorkerWrapper {
  constructor(workerPath) {
    this.worker = new Worker(workerPath);
    this.pendingTasks = new Map();
    this.taskId = 0;
    
    this.worker.onmessage = (event) => {
      this.handleMessage(event.data);
    };
    
    this.worker.onerror = (error) => {
      this.handleError(error);
    };
  }
  
  // Async task execution
  execute(action, data) {
    return new Promise((resolve, reject) => {
      const id = this.taskId++;
      
      this.pendingTasks.set(id, { resolve, reject });
      
      this.worker.postMessage({
        id,
        action,
        data
      });
    });
  }
  
  handleMessage(message) {
    const { id, result, error } = message;
    
    const task = this.pendingTasks.get(id);
    if (task) {
      this.pendingTasks.delete(id);
      
      if (error) {
        task.reject(new Error(error));
      } else {
        task.resolve(result);
      }
    }
  }
  
  handleError(error) {
    // Reject all pending tasks
    for (const [id, task] of this.pendingTasks) {
      task.reject(error);
    }
    this.pendingTasks.clear();
  }
  
  terminate() {
    this.worker.terminate();
    
    // Reject all pending tasks
    for (const [id, task] of this.pendingTasks) {
      task.reject(new Error('Worker terminated'));
    }
    this.pendingTasks.clear();
  }
}

// Corresponding worker.js
self.onmessage = async (event) => {
  const { id, action, data } = event.data;
  
  try {
    let result;
    
    switch (action) {
      case 'fibonacci':
        result = fibonacci(data);
        break;
      case 'sort':
        result = data.slice().sort((a, b) => a - b);
        break;
      default:
        throw new Error('Unknown action');
    }
    
    self.postMessage({ id, result });
  } catch (error) {
    self.postMessage({ id, error: error.message });
  }
};

function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

Worker Pool

class WorkerPool {
  constructor(workerPath, poolSize = navigator.hardwareConcurrency || 4) {
    this.workerPath = workerPath;
    this.poolSize = poolSize;
    this.workers = [];
    this.taskQueue = [];
    this.workerStatus = [];
    
    this.initPool();
  }
  
  initPool() {
    for (let i = 0; i < this.poolSize; i++) {
      const worker = new Worker(this.workerPath);
      
      worker.onmessage = (event) => {
        this.handleWorkerMessage(i, event.data);
      };
      
      worker.onerror = (error) => {
        this.handleWorkerError(i, error);
      };
      
      this.workers.push(worker);
      this.workerStatus.push({ busy: false, currentTask: null });
    }
  }
  
  execute(action, data) {
    return new Promise((resolve, reject) => {
      const task = { action, data, resolve, reject };
      
      const availableWorkerIndex = this.findAvailableWorker();
      
      if (availableWorkerIndex !== -1) {
        this.assignTask(availableWorkerIndex, task);
      } else {
        this.taskQueue.push(task);
      }
    });
  }
  
  findAvailableWorker() {
    return this.workerStatus.findIndex(status => !status.busy);
  }
  
  assignTask(workerIndex, task) {
    this.workerStatus[workerIndex] = {
      busy: true,
      currentTask: task
    };
    
    this.workers[workerIndex].postMessage({
      action: task.action,
      data: task.data
    });
  }
  
  handleWorkerMessage(workerIndex, result) {
    const status = this.workerStatus[workerIndex];
    
    if (status.currentTask) {
      status.currentTask.resolve(result);
    }
    
    this.workerStatus[workerIndex] = { busy: false, currentTask: null };
    
    // Process next task in queue
    if (this.taskQueue.length > 0) {
      const nextTask = this.taskQueue.shift();
      this.assignTask(workerIndex, nextTask);
    }
  }
  
  handleWorkerError(workerIndex, error) {
    const status = this.workerStatus[workerIndex];
    
    if (status.currentTask) {
      status.currentTask.reject(error);
    }
    
    this.workerStatus[workerIndex] = { busy: false, currentTask: null };
    
    // Recreate Worker
    this.workers[workerIndex].terminate();
    this.workers[workerIndex] = new Worker(this.workerPath);
  }
  
  terminate() {
    this.workers.forEach(worker => worker.terminate());
    this.workers = [];
    this.workerStatus = [];
    
    // Reject all pending tasks
    this.taskQueue.forEach(task => {
      task.reject(new Error('Worker pool terminated'));
    });
    this.taskQueue = [];
  }
  
  getStats() {
    return {
      poolSize: this.poolSize,
      busyWorkers: this.workerStatus.filter(s => s.busy).length,
      queuedTasks: this.taskQueue.length
    };
  }
}

// Usage
const pool = new WorkerPool('compute-worker.js', 4);

// Execute multiple tasks in parallel
const results = await Promise.all([
  pool.execute('fibonacci', 40),
  pool.execute('fibonacci', 41),
  pool.execute('fibonacci', 42),
  pool.execute('fibonacci', 43)
]);

console.log('Results:', results);
console.log('Stats:', pool.getStats());

Shared Workers

Basic Usage

// Main thread (can be shared across multiple pages)
const sharedWorker = new SharedWorker('shared-worker.js');
const port = sharedWorker.port;

// Must call start()
port.start();

// Send message
port.postMessage({ type: 'getData', key: 'user' });

// Receive message
port.onmessage = (event) => {
  console.log('Shared Worker returned:', event.data);
};

// Error handling
sharedWorker.onerror = (error) => {
  console.error('Shared Worker error:', error);
};
// shared-worker.js
const connections = new Set();
const sharedData = new Map();

self.onconnect = (event) => {
  const port = event.ports[0];
  connections.add(port);
  
  port.onmessage = (event) => {
    handleMessage(port, event.data);
  };
  
  port.start();
  
  // Notify connection count
  broadcastConnectionCount();
};

function handleMessage(port, message) {
  switch (message.type) {
    case 'setData':
      sharedData.set(message.key, message.value);
      // Broadcast update
      broadcast({ type: 'dataUpdated', key: message.key, value: message.value });
      break;
      
    case 'getData':
      port.postMessage({
        type: 'data',
        key: message.key,
        value: sharedData.get(message.key)
      });
      break;
      
    case 'broadcast':
      broadcast(message.data, port);
      break;
  }
}

function broadcast(message, excludePort = null) {
  connections.forEach(port => {
    if (port !== excludePort) {
      port.postMessage(message);
    }
  });
}

function broadcastConnectionCount() {
  broadcast({ type: 'connectionCount', count: connections.size });
}

Cross-Tab Communication

// shared-worker.js - Chat application example
const clients = new Map();
let clientId = 0;

self.onconnect = (event) => {
  const port = event.ports[0];
  const id = clientId++;
  
  clients.set(id, { port, username: null });
  
  port.onmessage = (event) => {
    handleMessage(id, event.data);
  };
  
  port.start();
  
  port.postMessage({ type: 'connected', clientId: id });
};

function handleMessage(clientId, message) {
  const client = clients.get(clientId);
  
  switch (message.type) {
    case 'setUsername':
      client.username = message.username;
      broadcastUserList();
      break;
      
    case 'sendMessage':
      broadcast({
        type: 'message',
        from: client.username,
        text: message.text,
        timestamp: Date.now()
      });
      break;
      
    case 'disconnect':
      clients.delete(clientId);
      broadcastUserList();
      break;
  }
}

function broadcast(message) {
  clients.forEach(client => {
    client.port.postMessage(message);
  });
}

function broadcastUserList() {
  const users = Array.from(clients.values())
    .filter(c => c.username)
    .map(c => c.username);
    
  broadcast({ type: 'userList', users });
}

Practical Applications

Image Processing

// Main thread
class ImageProcessor {
  constructor() {
    this.worker = new Worker('image-worker.js');
    this.pending = new Map();
    this.taskId = 0;
    
    this.worker.onmessage = (event) => {
      const { id, imageData, error } = event.data;
      const task = this.pending.get(id);
      
      if (task) {
        this.pending.delete(id);
        
        if (error) {
          task.reject(new Error(error));
        } else {
          task.resolve(imageData);
        }
      }
    };
  }
  
  async process(imageData, filters) {
    return new Promise((resolve, reject) => {
      const id = this.taskId++;
      this.pending.set(id, { resolve, reject });
      
      // Transfer ImageData buffer
      this.worker.postMessage(
        { id, imageData, filters },
        [imageData.data.buffer]
      );
    });
  }
  
  async applyFilter(canvas, filterType) {
    const ctx = canvas.getContext('2d');
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    
    const processed = await this.process(imageData, [filterType]);
    
    ctx.putImageData(processed, 0, 0);
  }
}

// image-worker.js
self.onmessage = (event) => {
  const { id, imageData, filters } = event.data;
  
  try {
    let data = new Uint8ClampedArray(imageData.data);
    
    filters.forEach(filter => {
      data = applyFilter(data, filter, imageData.width, imageData.height);
    });
    
    const result = new ImageData(data, imageData.width, imageData.height);
    
    self.postMessage(
      { id, imageData: result },
      [result.data.buffer]
    );
  } catch (error) {
    self.postMessage({ id, error: error.message });
  }
};

function applyFilter(data, filter, width, height) {
  const result = new Uint8ClampedArray(data.length);
  
  switch (filter) {
    case 'grayscale':
      for (let i = 0; i < data.length; i += 4) {
        const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
        result[i] = avg;
        result[i + 1] = avg;
        result[i + 2] = avg;
        result[i + 3] = data[i + 3];
      }
      break;
      
    case 'invert':
      for (let i = 0; i < data.length; i += 4) {
        result[i] = 255 - data[i];
        result[i + 1] = 255 - data[i + 1];
        result[i + 2] = 255 - data[i + 2];
        result[i + 3] = data[i + 3];
      }
      break;
      
    case 'blur':
      return applyBlur(data, width, height);
      
    default:
      return data;
  }
  
  return result;
}

function applyBlur(data, width, height) {
  const result = new Uint8ClampedArray(data.length);
  const kernel = [1, 2, 1, 2, 4, 2, 1, 2, 1];
  const kernelSum = 16;
  
  for (let y = 1; y < height - 1; y++) {
    for (let x = 1; x < width - 1; x++) {
      for (let c = 0; c < 3; c++) {
        let sum = 0;
        let k = 0;
        
        for (let ky = -1; ky <= 1; ky++) {
          for (let kx = -1; kx <= 1; kx++) {
            const idx = ((y + ky) * width + (x + kx)) * 4 + c;
            sum += data[idx] * kernel[k++];
          }
        }
        
        const idx = (y * width + x) * 4 + c;
        result[idx] = sum / kernelSum;
      }
      
      const idx = (y * width + x) * 4 + 3;
      result[idx] = data[idx];
    }
  }
  
  return result;
}

Big Data Processing

// Main thread
class DataProcessor {
  constructor() {
    this.pool = new WorkerPool('data-worker.js', 4);
  }
  
  async sortLargeArray(data) {
    // Chunk processing
    const chunkSize = Math.ceil(data.length / 4);
    const chunks = [];
    
    for (let i = 0; i < data.length; i += chunkSize) {
      chunks.push(data.slice(i, i + chunkSize));
    }
    
    // Sort chunks in parallel
    const sortedChunks = await Promise.all(
      chunks.map(chunk => this.pool.execute('sort', chunk))
    );
    
    // Merge sorted chunks
    return this.mergeArrays(sortedChunks);
  }
  
  mergeArrays(arrays) {
    // K-way merge
    const result = [];
    const indices = arrays.map(() => 0);
    
    while (true) {
      let minVal = Infinity;
      let minIdx = -1;
      
      for (let i = 0; i < arrays.length; i++) {
        if (indices[i] < arrays[i].length && arrays[i][indices[i]] < minVal) {
          minVal = arrays[i][indices[i]];
          minIdx = i;
        }
      }
      
      if (minIdx === -1) break;
      
      result.push(minVal);
      indices[minIdx]++;
    }
    
    return result;
  }
  
  async aggregateData(data, groupBy, aggregations) {
    return this.pool.execute('aggregate', { data, groupBy, aggregations });
  }
}

// data-worker.js
self.onmessage = (event) => {
  const { action, data } = event.data;
  
  switch (action) {
    case 'sort':
      self.postMessage(data.slice().sort((a, b) => a - b));
      break;
      
    case 'aggregate':
      self.postMessage(aggregate(data.data, data.groupBy, data.aggregations));
      break;
      
    case 'filter':
      self.postMessage(data.array.filter(item => evalCondition(item, data.condition)));
      break;
  }
};

function aggregate(data, groupBy, aggregations) {
  const groups = new Map();
  
  data.forEach(item => {
    const key = item[groupBy];
    if (!groups.has(key)) {
      groups.set(key, []);
    }
    groups.get(key).push(item);
  });
  
  const result = [];
  
  groups.forEach((items, key) => {
    const row = { [groupBy]: key };
    
    aggregations.forEach(agg => {
      const values = items.map(item => item[agg.field]);
      
      switch (agg.type) {
        case 'sum':
          row[agg.alias] = values.reduce((a, b) => a + b, 0);
          break;
        case 'avg':
          row[agg.alias] = values.reduce((a, b) => a + b, 0) / values.length;
          break;
        case 'count':
          row[agg.alias] = values.length;
          break;
        case 'min':
          row[agg.alias] = Math.min(...values);
          break;
        case 'max':
          row[agg.alias] = Math.max(...values);
          break;
      }
    });
    
    result.push(row);
  });
  
  return result;
}

Best Practices Summary

Web Workers Best Practices:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   When to Use                                       │
│   ├── CPU-intensive computations                   │
│   ├── Large data processing                        │
│   ├── Image/video processing                       │
│   └── Complex algorithm execution                  │
│                                                     │
│   Performance Optimization                          │
│   ├── Use transferable objects to reduce copying   │
│   ├── Implement worker pool for concurrency        │
│   ├── Avoid frequent Worker creation/destruction   │
│   └── Choose appropriate task granularity          │
│                                                     │
│   Considerations                                    │
│   ├── Workers cannot access DOM                    │
│   ├── Communication has serialization overhead     │
│   ├── Debugging is more complex                    │
│   └── Mind memory management                       │
│                                                     │
└─────────────────────────────────────────────────────┘
Worker TypeCharacteristicsUse Case
DedicatedSingle page onlyIn-page computation
SharedShared across pagesCross-tab communication
ServiceNetwork proxyOffline cache, push

Master Web Workers to unlock JavaScript’s multi-threading potential.