Skip to main content
Web Workers allow you to run JavaScript in background threads, keeping your UI responsive during heavy computations. Webpack 5 provides built-in support for Web Workers.

What are Web Workers?

Web Workers run scripts in background threads separate from the main execution thread, enabling:
  • Parallel processing without blocking the UI
  • Heavy computations without freezing the page
  • Better performance for CPU-intensive tasks
Web Workers run in a separate global context and don’t have access to the DOM or window object.

Basic Usage

1

Create a worker file

worker.js:
self.addEventListener('message', (event) => {
  const result = heavyComputation(event.data);
  self.postMessage(result);
});

function heavyComputation(data) {
  // Perform CPU-intensive work
  let result = 0;
  for (let i = 0; i < data; i++) {
    result += Math.sqrt(i);
  }
  return result;
}
2

Use the worker in your code

main.js:
const worker = new Worker(
  new URL('./worker.js', import.meta.url)
);

worker.postMessage(1000000);

worker.addEventListener('message', (event) => {
  console.log('Result:', event.data);
});
3

Configure webpack

module.exports = {
  output: {
    filename: '[name].js',
    chunkFilename: '[name].js'
  }
};
No additional configuration needed - webpack 5 handles workers automatically!
Use new URL('./worker.js', import.meta.url) syntax for webpack to automatically bundle the worker.

Worker Syntax

Webpack 5 supports multiple worker syntaxes:

Using new URL()

const worker = new Worker(
  new URL('./worker.js', import.meta.url)
);

With Type

const worker = new Worker(
  new URL('./worker.js', import.meta.url),
  { type: 'module' }
);

Named Chunks

const worker = new Worker(
  new URL(
    /* webpackChunkName: "my-worker" */
    './worker.js',
    import.meta.url
  )
);

Worker Configuration

Configure how workers are bundled:
module.exports = {
  output: {
    filename: '[name].js',
    chunkFilename: '[name].worker.js',
    workerChunkLoading: 'import', // or 'import-scripts'
  },
  optimization: {
    concatenateModules: true,
    usedExports: true
  }
};

Shared Workers

Shared Workers can be accessed by multiple scripts: shared-worker.js:
let connections = 0;

self.addEventListener('connect', (event) => {
  const port = event.ports[0];
  connections++;

  port.addEventListener('message', (e) => {
    // Broadcast to all connections
    port.postMessage(`Total connections: ${connections}`);
  });

  port.start();
});
main.js:
const worker = new SharedWorker(
  new URL('./shared-worker.js', import.meta.url)
);

worker.port.start();
worker.port.postMessage('hello');

worker.port.addEventListener('message', (event) => {
  console.log(event.data);
});

Service Workers

Service Workers provide offline capabilities and caching:
// Register service worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register(
    new URL('./service-worker.js', import.meta.url)
  );
}
service-worker.js:
const CACHE_NAME = 'my-app-v1';

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll([
        '/',
        '/styles.css',
        '/script.js'
      ]);
    })
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});
Service Workers require HTTPS in production (except for localhost).

Worker Communication Patterns

Simple Request-Response

// Main thread
const worker = new Worker(new URL('./worker.js', import.meta.url));

worker.postMessage({ type: 'calculate', data: 100 });

worker.addEventListener('message', (event) => {
  console.log('Result:', event.data);
});

// Worker
self.addEventListener('message', (event) => {
  if (event.data.type === 'calculate') {
    const result = performCalculation(event.data.data);
    self.postMessage(result);
  }
});

Promise-Based Communication

class WorkerPool {
  constructor(workerPath, poolSize = 4) {
    this.workers = [];
    this.taskQueue = [];
    
    for (let i = 0; i < poolSize; i++) {
      const worker = new Worker(new URL(workerPath, import.meta.url));
      this.workers.push({ worker, busy: false });
    }
  }

  async execute(data) {
    return new Promise((resolve, reject) => {
      const availableWorker = this.workers.find(w => !w.busy);
      
      if (availableWorker) {
        this.runTask(availableWorker, data, resolve, reject);
      } else {
        this.taskQueue.push({ data, resolve, reject });
      }
    });
  }

  runTask(workerObj, data, resolve, reject) {
    workerObj.busy = true;
    
    const handleMessage = (event) => {
      workerObj.worker.removeEventListener('message', handleMessage);
      workerObj.busy = false;
      resolve(event.data);
      this.processQueue();
    };
    
    workerObj.worker.addEventListener('message', handleMessage);
    workerObj.worker.postMessage(data);
  }

  processQueue() {
    if (this.taskQueue.length > 0) {
      const availableWorker = this.workers.find(w => !w.busy);
      if (availableWorker) {
        const { data, resolve, reject } = this.taskQueue.shift();
        this.runTask(availableWorker, data, resolve, reject);
      }
    }
  }
}

// Usage
const pool = new WorkerPool('./worker.js', 4);
const result = await pool.execute({ task: 'compute', value: 1000 });

Transferable Objects

Transfer ownership of data for better performance:
// Create large data
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
const data = new Uint8Array(buffer);

// Transfer buffer to worker (zero-copy)
worker.postMessage(data, [buffer]);

// buffer is now empty in main thread
console.log(buffer.byteLength); // 0
Worker:
self.addEventListener('message', (event) => {
  const data = event.data;
  // Process data
  
  // Transfer back
  self.postMessage(data, [data.buffer]);
});
Use transferable objects for large ArrayBuffers to avoid copying and improve performance.

TypeScript Support

worker.ts:
interface WorkerMessage {
  type: 'calculate' | 'process';
  data: number[];
}

interface WorkerResponse {
  result: number;
  duration: number;
}

self.addEventListener('message', (event: MessageEvent<WorkerMessage>) => {
  const start = performance.now();
  
  let result = 0;
  if (event.data.type === 'calculate') {
    result = event.data.data.reduce((sum, n) => sum + n, 0);
  }
  
  const response: WorkerResponse = {
    result,
    duration: performance.now() - start
  };
  
  self.postMessage(response);
});
main.ts:
const worker = new Worker(
  new URL('./worker.ts', import.meta.url)
);

worker.postMessage({
  type: 'calculate',
  data: [1, 2, 3, 4, 5]
});

worker.addEventListener('message', (event) => {
  console.log('Result:', event.data.result);
  console.log('Duration:', event.data.duration);
});

Error Handling

const worker = new Worker(new URL('./worker.js', import.meta.url));

// Handle messages
worker.addEventListener('message', (event) => {
  console.log('Success:', event.data);
});

// Handle errors
worker.addEventListener('error', (error) => {
  console.error('Worker error:', error.message);
  console.error('File:', error.filename);
  console.error('Line:', error.lineno);
});

// Handle unhandled rejections in worker
worker.addEventListener('messageerror', (event) => {
  console.error('Message error:', event);
});

// In worker
self.addEventListener('error', (error) => {
  console.error('Worker internal error:', error);
});

self.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled promise rejection:', event.reason);
});

Use Cases

Image Processing

// worker.js
self.addEventListener('message', (event) => {
  const { imageData, filter } = event.data;
  
  const processed = applyFilter(imageData, filter);
  
  self.postMessage(processed, [processed.data.buffer]);
});

function applyFilter(imageData, filter) {
  // Process pixels
  for (let i = 0; i < imageData.data.length; i += 4) {
    // Apply filter transformations
    imageData.data[i] *= filter.r;
    imageData.data[i + 1] *= filter.g;
    imageData.data[i + 2] *= filter.b;
  }
  return imageData;
}

Data Processing

// worker.js
self.addEventListener('message', async (event) => {
  const { data, operation } = event.data;
  
  const result = await processLargeDataset(data, operation);
  
  self.postMessage({ result });
});

async function processLargeDataset(data, operation) {
  // Process millions of records
  return data.map(item => {
    switch(operation) {
      case 'transform':
        return transformItem(item);
      case 'filter':
        return filterItem(item);
      default:
        return item;
    }
  });
}

Background Sync

// worker.js
let syncQueue = [];

self.addEventListener('message', (event) => {
  if (event.data.type === 'sync') {
    syncQueue.push(event.data.payload);
    processSyncQueue();
  }
});

async function processSyncQueue() {
  while (syncQueue.length > 0) {
    const item = syncQueue.shift();
    try {
      await fetch('/api/sync', {
        method: 'POST',
        body: JSON.stringify(item)
      });
      self.postMessage({ type: 'sync-complete', id: item.id });
    } catch (error) {
      // Retry later
      syncQueue.push(item);
      await new Promise(resolve => setTimeout(resolve, 5000));
    }
  }
}

Best Practices

1

Use for CPU-intensive tasks

Offload heavy computations that would block the UI:
  • Image/video processing
  • Large data transformations
  • Complex calculations
  • Encryption/decryption
2

Transfer large data efficiently

Use transferable objects for ArrayBuffers:
worker.postMessage(data, [data.buffer]);
3

Handle errors gracefully

Always add error listeners:
worker.addEventListener('error', handleError);
4

Terminate workers when done

worker.terminate();
Workers can’t access the DOM, window object, or parent page directly. Communicate only through messages.