# What Are Web Workers and How They Enable Multi-Threading in JavaScript

> Discover Web Workers, a browser API enabling true JavaScript multi-threading. Run code in background threads, boosting performance without UI interference. Learn more now.

- Repository: [Leonardo Maldonado/33-js-concepts](https://github.com/leonardomso/33-js-concepts)
- Tags: deep-dive
- Published: 2026-03-03

---

**Web Workers are a browser-level API that allows JavaScript to execute in background threads separate from the main UI thread, enabling true parallelism through message-based communication while maintaining DOM isolation.**

Web Workers solve the single-threaded limitation of JavaScript by moving CPU-intensive operations off the main execution thread. According to the leonardomso/33-js-concepts repository, these workers run in complete isolation with their own event loops and global scopes, allowing complex calculations to proceed without blocking user interactions or rendering.

## How Web Workers Enable True Parallelism

### Background Threads and Global Scope Isolation

Each Web Worker operates within its own `DedicatedWorkerGlobalScope`, completely separate from the main thread's `window` object. Workers **cannot access the DOM**, `document`, `localStorage`, or any UI-related APIs, creating a sandboxed environment that prevents race conditions with the rendering engine. Because every worker maintains an independent event loop, the main thread remains responsive to user input while workers process data on separate OS threads.

### Message Passing via Structured-Clone Algorithm

Communication between threads occurs exclusively through asynchronous message passing using `worker.postMessage(data)` and `self.onmessage` handlers. The browser uses the **structured-clone algorithm** to serialize data, creating deep copies of objects sent between contexts. For large binary data, **transferable objects** (such as `ArrayBuffer` or `OffscreenCanvas`) move ownership between threads without copying, immediately detaching the buffer from the sender to avoid memory duplication costs.

### Types of Workers and Instantiation Syntax

The API provides three distinct worker categories with different use cases:

- **Dedicated Workers**: Owned by a single script, created via `new Worker('worker.js')` for classic scripts or `new Worker('worker.js', { type: 'module' })` for ES modules
- **Shared Workers**: Accessible from multiple browsing contexts (tabs, iframes) using the same origin
- **Service Workers**: Act as network proxies for offline capabilities (distinct from threading workers)

True multi-threading emerges when spawning multiple dedicated workers simultaneously; each runs on a separate CPU core, achieving parallelism impossible with single-threaded asynchronous callbacks alone.

## Implementing Web Workers: Code Examples

### Basic Dedicated Worker Setup

The classic implementation pattern from `docs/concepts/web-workers.mdx` demonstrates the fundamental request-response cycle between main thread and worker:

**main.js**

```javascript
// Create a worker
const worker = new Worker('worker.js')

// Send data to the worker
worker.postMessage({ numbers: [1, 2, 3, 4, 5] })

// Receive result
worker.onmessage = e => {
  console.log('Result from worker:', e.data)
}

```

**worker.js**

```javascript
self.onmessage = e => {
  const { numbers } = e.data
  // Heavy computation – sum the numbers
  const sum = numbers.reduce((a, b) => a + b, 0)
  // Send result back
  self.postMessage({ sum })
}

```

### Modern ES Module Workers

Modern browsers support module workers, allowing `import`/`export` syntax instead of legacy `importScripts()`:

**main.js**

```javascript
const worker = new Worker('worker-module.js', { type: 'module' })
worker.postMessage({ task: 'process', data: [1, 2, 3] })
worker.onmessage = e => console.log('Result:', e.data)

```

**worker-module.js**

```javascript
import { processData } from './utils.js'   // Regular ES‑module import

self.onmessage = e => {
  const { task, data } = e.data
  if (task === 'process') {
    const result = processData(data)
    self.postMessage(result)
  }
}

```

### Zero-Copy Performance with Transferable Objects

For memory-intensive applications, transfer ownership of `ArrayBuffer` instances to eliminate serialization overhead:

**main.js**

```javascript
const hugeBuffer = new ArrayBuffer(100 * 1024 * 1024) // 100 MB
const uint8 = new Uint8Array(hugeBuffer)
// …fill the buffer…
worker.postMessage(hugeBuffer, [hugeBuffer]) // Transfer ownership
console.log(hugeBuffer.byteLength) // 0 – buffer is now detached

```

**worker.js**

```javascript
self.onmessage = e => {
  const buffer = e.data          // Receives the transferred buffer
  console.log(buffer.byteLength) // 104857600
  // Do work, then optionally transfer back
  self.postMessage(buffer, [buffer])
}

```

### Building a Reusable Worker Pool

Creating workers incurs significant overhead; the repository provides a `WorkerPool` class that maintains a fixed set of workers matching available CPU cores:

**WorkerPool.js**

```javascript
export class WorkerPool {
  constructor(script, size = navigator.hardwareConcurrency || 4) {
    this.workers = Array.from({ length: size }, () => ({
      worker: new Worker(script, { type: 'module' }),
      busy: false
    }))
    this.queue = []
  }
  runTask(data) {
    return new Promise((resolve, reject) => {
      const task = { data, resolve, reject }
      const free = this.workers.find(w => !w.busy)
      free ? this._run(free, task) : this.queue.push(task)
    })
  }
  _run(info, task) {
    info.busy = true
    const onMsg = e => {
      info.worker.removeEventListener('message', onMsg)
      info.busy = false
      task.resolve(e.data)
      if (this.queue.length) this._run(info, this.queue.shift())
    }
    const onErr = err => {
      info.worker.removeEventListener('error', onErr)
      info.busy = false
      task.reject(err)
    }
    info.worker.addEventListener('message', onMsg)
    info.worker.addEventListener('error', onErr)
    info.worker.postMessage(task.data)
  }
  terminate() {
    this.workers.forEach(w => w.worker.terminate())
    this.workers = []
    this.queue = []
  }
}

```

**main.js**

```javascript
import { WorkerPool } from './WorkerPool.js'
const pool = new WorkerPool('compute-worker.js', 4)

async function batchProcess(items) {
  const results = await Promise.all(items.map(item => pool.runTask(item)))
  return results
}

batchProcess([{ id: 1 }, { id: 2 }, { id: 3 }]).then(console.log).finally(() => pool.terminate())

```

## Summary

- **Web Workers** execute JavaScript on background threads separate from the main UI thread, enabling true multi-threading in browsers.
- Each worker runs in an isolated **global scope** (`DedicatedWorkerGlobalScope`) without DOM access, communicating exclusively via `postMessage` using structured cloning or transferable objects.
- **Dedicated Workers** suit single-page heavy computation, while **Module Workers** support modern ES6 `import` syntax via `{ type: 'module' }`.
- **Transferable Objects** (like `ArrayBuffer`) move data between threads without copying, critical for high-performance applications handling large datasets.
- **Worker Pools** reduce instantiation overhead by reusing a fixed number of workers (typically `navigator.hardwareConcurrency`) to process task queues efficiently.

## Frequently Asked Questions

### Can Web Workers directly manipulate the DOM?

No. Web Workers operate in a completely isolated environment without access to the `window` object, `document`, or any DOM-related APIs. They cannot read or modify UI elements, which prevents race conditions but requires all UI updates to be marshaled back to the main thread via `postMessage`.

### How do Web Workers communicate with the main thread?

Communication occurs exclusively through asynchronous message passing. The main thread calls `worker.postMessage(data)` to send data, while the worker listens via `self.onmessage`. The browser serializes data using the structured-clone algorithm by default, or transfers ownership for `ArrayBuffer` and similar transferable types to avoid memory duplication.

### What is the difference between Dedicated Workers and Shared Workers?

**Dedicated Workers** are instantiated by and accessible to only a single script context (one tab or window), making them ideal for page-specific calculations. **Shared Workers** can be accessed from multiple browsing contexts (different tabs, windows, or iframes) sharing the same origin, allowing coordinated background processing across multiple visible instances of an application.

### How many Web Workers should an application create?

Limit active workers to `navigator.hardwareConcurrency` (typically matching CPU core count) to avoid context-switching overhead. For dynamic workloads, implement a **Worker Pool** that reuses a fixed set of workers rather than instantiating new ones per task, as worker creation carries significant initialization costs in terms of memory and startup time.