Web Workers in JavaScript

Web Workers provide a way to run JavaScript in the background, separate from the main browser thread. This allows for multi-threading in JavaScript, enabling CPU-intensive tasks to run without blocking the user interface.

Introduction to Web Workers

JavaScript traditionally runs in a single thread, which means that long-running or complex operations can block the UI and make your application unresponsive. Web Workers solve this problem by allowing you to run scripts in background threads.

Note: Web Workers run in an isolated context. They don't have access to the DOM, the window object, or the parent object. They can only communicate with the main thread through messages.

Types of Web Workers

There are three main types of Web Workers:

1. Dedicated Workers

A dedicated worker is the most common type, used by a single script instance. It can only communicate with the script that created it.

2. Shared Workers

A shared worker can be accessed by multiple scripts running in different windows, iframes, or even workers. They're useful for sharing resources between different parts of your application.

3. Service Workers

Service workers act as proxy servers that sit between web applications, the browser, and the network. They're primarily used for features like offline support, push notifications, and background sync.

Creating a Dedicated Worker

To create a dedicated worker, you need two files: the main JavaScript file and a separate worker file.

// Main script (main.js)
// Create a new worker
const worker = new Worker('worker.js');

// Send a message to the worker
worker.postMessage({ command: 'start', data: [1, 2, 3, 4, 5] });

// Receive messages from the worker
worker.onmessage = function(event) {
  console.log('Received from worker:', event.data);
};

// Handle errors
worker.onerror = function(error) {
  console.error('Worker error:', error.message);
};
// Worker script (worker.js)
// Listen for messages from the main thread
self.onmessage = function(event) {
  console.log('Received from main thread:', event.data);
  
  if (event.data.command === 'start') {
    // Perform some CPU-intensive task
    const result = processData(event.data.data);
    
    // Send the result back to the main thread
    self.postMessage({ result: result });
  }
};

// Example of a CPU-intensive function
function processData(data) {
  // Simulate heavy computation
  let result = 0;
  for (let i = 0; i < 10000000; i++) {
    result += Math.sqrt(i);
  }
  
  // Process the actual data
  return data.map(x => x * x);
}

Communication Between Workers and the Main Thread

Web Workers communicate with the main thread using a messaging system:

Sending Messages

You can send messages using the postMessage() method. The message can be any value that can be cloned using the structured clone algorithm, which includes most JavaScript objects and values.

// From main thread to worker
worker.postMessage({
  command: 'process',
  data: [1, 2, 3, 4, 5],
  options: {
    multiply: 2,
    add: 1
  }
});

// From worker to main thread
self.postMessage({
  status: 'complete',
  result: [3, 5, 7, 9, 11]
});

Receiving Messages

You can receive messages by listening to the message event:

// In main thread
worker.addEventListener('message', function(event) {
  console.log('Received from worker:', event.data);
});

// Alternative syntax
worker.onmessage = function(event) {
  console.log('Received from worker:', event.data);
};

// In worker
self.addEventListener('message', function(event) {
  console.log('Received from main thread:', event.data);
});

// Alternative syntax
self.onmessage = function(event) {
  console.log('Received from main thread:', event.data);
};

Terminating a Worker

You can terminate a worker from the main thread or from within the worker itself:

// From main thread
worker.terminate();

// From within the worker
self.close();

Warning: Terminating a worker will immediately stop all operations. Any pending operations or messages will be lost.

Importing Scripts in Workers

Workers can import additional scripts using the importScripts() function:

// In worker.js
importScripts('helper1.js', 'helper2.js');

// Now you can use functions and variables defined in those scripts
const result = helperFunction(data);

Shared Workers

Shared Workers allow multiple scripts to communicate with a single worker. This is useful for sharing resources between different parts of your application.

// Main script (in multiple pages or iframes)
const sharedWorker = new SharedWorker('shared-worker.js');

// Get a reference to the port
const port = sharedWorker.port;

// Start the port
port.start();

// Send a message to the shared worker
port.postMessage({ command: 'register', id: 'page1' });

// Receive messages from the shared worker
port.onmessage = function(event) {
  console.log('Received from shared worker:', event.data);
};
// Shared worker script (shared-worker.js)
const connections = [];

// Listen for connections
self.onconnect = function(event) {
  const port = event.ports[0];
  connections.push(port);
  
  // Start the port
  port.start();
  
  // Listen for messages on this port
  port.onmessage = function(event) {
    console.log('Shared worker received:', event.data);
    
    if (event.data.command === 'broadcast') {
      // Broadcast a message to all connected ports
      connections.forEach(connection => {
        connection.postMessage({
          type: 'broadcast',
          message: event.data.message,
          from: event.data.id
        });
      });
    }
  };
};

Transferable Objects

For large data transfers, you can use transferable objects to avoid the cost of copying the data. When you transfer an object, it becomes unusable in the sender's context.

// Create a large array buffer
const buffer = new ArrayBuffer(100 * 1024 * 1024); // 100MB

// Fill the buffer with data
const view = new Uint8Array(buffer);
for (let i = 0; i < view.length; i++) {
  view[i] = i % 256;
}

// Transfer the buffer to the worker (not copying it)
worker.postMessage({ buffer: buffer }, [buffer]);

// After the transfer, buffer is neutered (zero length)
console.log(buffer.byteLength); // 0

Worker Scope and Limitations

Web Workers have access to a limited set of JavaScript features:

Available Features

  • The navigator object
  • The location object (read-only)
  • XMLHttpRequest and fetch
  • setTimeout/setInterval
  • The Application Cache
  • Importing external scripts with importScripts()
  • Creating other workers
  • WebSockets and IndexedDB

Unavailable Features

  • The DOM (document, window, etc.)
  • The parent object
  • The localStorage and sessionStorage objects
  • Access to the main thread's global variables

Practical Examples

Example 1: Image Processing

Web Workers are ideal for image processing tasks, which can be CPU-intensive:

// Main script
const worker = new Worker('image-processor.js');

// Get the image data from a canvas
const canvas = document.getElementById('sourceCanvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

// Send the image data to the worker
worker.postMessage({
  imageData: imageData,
  filter: 'grayscale'
});

// Receive the processed image data
worker.onmessage = function(event) {
  // Draw the processed image data on another canvas
  const resultCanvas = document.getElementById('resultCanvas');
  const resultCtx = resultCanvas.getContext('2d');
  resultCtx.putImageData(event.data.imageData, 0, 0);
};
// image-processor.js
self.onmessage = function(event) {
  const imageData = event.data.imageData;
  const filter = event.data.filter;
  
  // Apply the requested filter
  switch(filter) {
    case 'grayscale':
      applyGrayscale(imageData);
      break;
    case 'invert':
      applyInvert(imageData);
      break;
    // Add more filters as needed
  }
  
  // Send the processed image data back
  self.postMessage({ imageData: imageData });
};

function applyGrayscale(imageData) {
  const data = imageData.data;
  for (let i = 0; i < data.length; i += 4) {
    const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
    data[i] = avg;     // Red
    data[i + 1] = avg; // Green
    data[i + 2] = avg; // Blue
    // data[i + 3] is Alpha (unchanged)
  }
}

function applyInvert(imageData) {
  const data = imageData.data;
  for (let i = 0; i < data.length; i += 4) {
    data[i] = 255 - data[i];         // Red
    data[i + 1] = 255 - data[i + 1]; // Green
    data[i + 2] = 255 - data[i + 2]; // Blue
    // data[i + 3] is Alpha (unchanged)
  }
}

Example 2: Data Processing

Web Workers can be used for processing large datasets without blocking the UI:

// Main script
const worker = new Worker('data-processor.js');

// Fetch a large dataset
fetch('large-dataset.json')
  .then(response => response.json())
  .then(data => {
    // Send the data to the worker
    worker.postMessage({
      command: 'analyze',
      data: data
    });
    
    // Update UI to show processing status
    document.getElementById('status').textContent = 'Processing...';
  });

// Receive the processed results
worker.onmessage = function(event) {
  // Update the UI with the results
  document.getElementById('status').textContent = 'Complete';
  document.getElementById('results').textContent = JSON.stringify(event.data.results, null, 2);
};
// data-processor.js
self.onmessage = function(event) {
  if (event.data.command === 'analyze') {
    const data = event.data.data;
    
    // Perform complex analysis
    const results = analyzeData(data);
    
    // Send the results back
    self.postMessage({
      status: 'complete',
      results: results
    });
  }
};

function analyzeData(data) {
  // Example: Calculate statistics for a dataset
  const results = {
    count: data.length,
    averages: {},
    totals: {},
    min: {},
    max: {}
  };
  
  // Skip if no data
  if (data.length === 0) return results;
  
  // Get all numeric properties from the first item
  const numericProps = Object.keys(data[0]).filter(key => 
    typeof data[0][key] === 'number'
  );
  
  // Initialize results
  numericProps.forEach(prop => {
    results.totals[prop] = 0;
    results.min[prop] = Infinity;
    results.max[prop] = -Infinity;
  });
  
  // Process each item
  data.forEach(item => {
    numericProps.forEach(prop => {
      results.totals[prop] += item[prop];
      results.min[prop] = Math.min(results.min[prop], item[prop]);
      results.max[prop] = Math.max(results.max[prop], item[prop]);
    });
  });
  
  // Calculate averages
  numericProps.forEach(prop => {
    results.averages[prop] = results.totals[prop] / data.length;
  });
  
  return results;
}

Interactive Example

Try out this interactive example to see Web Workers in action:

Results will appear here

Best Practices

  • Use workers for CPU-intensive tasks: Image processing, data analysis, complex calculations
  • Keep the main thread free for UI updates: Move non-UI work to workers
  • Be mindful of communication overhead: Transferring large amounts of data between threads can be expensive
  • Use transferable objects for large data: ArrayBuffer, MessagePort, ImageBitmap
  • Terminate workers when they're no longer needed: This frees up resources
  • Handle errors: Always implement error handling for your workers
  • Consider a worker pool: For multiple tasks, reuse workers instead of creating new ones

Browser Support

Web Workers are supported in all modern browsers, including:

  • Chrome 4+
  • Firefox 3.5+
  • Safari 4+
  • Edge 12+
  • Opera 10.6+

Next Steps

Now that you understand Web Workers, you might want to explore:

  • Service Workers for offline applications and push notifications
  • Worklets for specialized rendering tasks
  • WebAssembly for high-performance computing in the browser
  • Worker libraries like Comlink for easier communication