SalakCode SalakCode
Browser APIs

Web Workers

Offload heavy computation from the main thread

Intermediate
performance multithreading browser

Definition

Web Workers allow you to run JavaScript in background threads separate from the main execution thread. This enables CPU-intensive tasks—like complex calculations, image processing, or data parsing—to run without blocking the UI or causing frame drops.

Why Web Workers?

JavaScript is single-threaded. When you run heavy computation:

// Main thread - UI freezes during this loop
function heavyCalculation() {
  let result = 0;
  for (let i = 0; i < 1000000000; i++) {
    result += Math.sqrt(i);
  }
  return result;
}

// User can't click, scroll, or interact while this runs

Web Workers solve this by running code on separate threads.

Creating a Web Worker

Basic Setup

// worker.js - Runs in separate thread
self.onmessage = function(e) {
  const { data } = e;
  const result = heavyCalculation(data);
  self.postMessage(result);
};

function heavyCalculation(input) {
  // CPU-intensive work here
  let result = 0;
  for (let i = 0; i < input; i++) {
    result += Math.sqrt(i);
  }
  return result;
}
// main.js - Main thread
const worker = new Worker('worker.js');

worker.onmessage = function(e) {
  console.log('Result:', e.data);
  // UI remains responsive throughout!
};

// Start the calculation
worker.postMessage(1000000000);

Inline Workers

For bundler compatibility, create workers inline:

const workerCode = `
  self.onmessage = function(e) {
    const result = e.data * 2;
    self.postMessage(result);
  };
`;

const blob = new Blob([workerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));

Communication Patterns

Request-Response

// Worker
let requestId = 0;
const pending = new Map();

self.onmessage = function(e) {
  const { id, payload } = e.data;
  const result = processData(payload);
  self.postMessage({ id, result });
};

// Main thread
function callWorker(data) {
  return new Promise((resolve) => {
    const id = ++requestId;
    pending.set(id, resolve);
    worker.postMessage({ id, payload: data });
  });
}

worker.onmessage = function(e) {
  const { id, result } = e.data;
  const resolve = pending.get(id);
  if (resolve) {
    resolve(result);
    pending.delete(id);
  }
};

SharedArrayBuffer

For high-performance shared memory (requires Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp headers):

// Create shared buffer
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Int32Array(sharedBuffer);

// Send to worker (no copy - shares memory)
worker.postMessage({ buffer: sharedBuffer });

// Both main thread and worker can access sharedArray
// Use Atomics for thread-safe operations
Atomics.store(sharedArray, 0, 42);
Atomics.add(sharedArray, 1, 1);

Worker Limitations

Web Workers don’t have access to:

  • DOM - Can’t manipulate the page directly
  • window object - Only self/WorkerGlobalScope
  • parent page - Can only communicate via postMessage
  • Some APIs - alert(), confirm(), localStorage, sessionStorage

Available APIs include:

  • fetch()
  • WebSockets
  • IndexedDB
  • setTimeout/setInterval
  • XMLHttpRequest

Use Cases

1. Image Processing

// worker.js
self.onmessage = async function(e) {
  const { imageData, filters } = e.data;
  
  // Apply filters pixel by pixel
  const processed = applyFilters(imageData, filters);
  
  self.postMessage(processed, [processed.data.buffer]);
};

2. Data Parsing

// Parse massive CSV without freezing UI
worker.postMessage({ type: 'parse', csv: hugeCsvString });

worker.onmessage = function(e) {
  const { rows, errors } = e.data;
  updateTable(rows);
  showErrors(errors);
};

3. Cryptographic Operations

// Hash large files
async function hashFile(file) {
  const worker = new Worker('hash-worker.js');
  const arrayBuffer = await file.arrayBuffer();
  
  return new Promise((resolve) => {
    worker.onmessage = (e) => resolve(e.data);
    worker.postMessage(arrayBuffer, [arrayBuffer]);
  });
}

Performance Considerations

Worker Pool

Creating workers has overhead. Use a pool for frequent tasks:

class WorkerPool {
  constructor(workerScript, poolSize = 4) {
    this.workers = Array(poolSize).fill(null).map(() => ({
      worker: new Worker(workerScript),
      busy: false,
      queue: []
    }));
  }

  async execute(data) {
    const available = this.workers.find(w => !w.busy);
    
    if (available) {
      return this.runWorker(available, data);
    }
    
    // Wait for next available worker
    return new Promise((resolve) => {
      this.queue.push({ data, resolve });
    });
  }

  runWorker(workerObj, data) {
    workerObj.busy = true;
    
    return new Promise((resolve) => {
      workerObj.worker.onmessage = (e) => {
        workerObj.busy = false;
        resolve(e.data);
        
        // Process queue
        if (this.queue.length > 0) {
          const next = this.queue.shift();
          this.execute(next.data).then(next.resolve);
        }
      };
      
      workerObj.worker.postMessage(data);
    });
  }
}

Transferable Objects

For large data, transfer ownership (zero-copy):

const hugeArray = new Uint8Array(100000000);

// Transfer ownership to worker (array becomes unusable in main thread)
worker.postMessage(hugeArray, [hugeArray.buffer]);

Terminating Workers

Always clean up workers to free memory:

// Method 1: From main thread
worker.terminate();

// Method 2: Worker self-termination
self.close();
Key Takeaway

Use Web Workers for CPU-intensive tasks that would block the main thread. Keep workers for long-running operations, use transferable objects for large data, implement worker pools for frequent tasks, and always terminate workers when done. Remember: workers can’t access the DOM, so all UI updates must happen via postMessage.

Modern Alternatives

  • WebAssembly - Near-native performance for compute-heavy tasks
  • Service Workers - For caching and offline functionality (different use case)
  • Comlink - Library that makes workers feel like async functions
// With Comlink
import * as Comlink from 'comlink';

const worker = new Worker('./worker.js');
const api = Comlink.wrap(worker);

const result = await api.heavyCalculation(1000000); // Like a regular async function!

Resources

Related Topics