# Web Workers

> Learn how to use Web Workers to run JavaScript in a background thread, communicating with the main page through postMessage so heavy work won't block the UI.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2018-04-16 | Updated: 2026-07-18 | Topics: [Web Platform](https://flaviocopes.com/tags/platform/) | Canonical: https://flaviocopes.com/web-workers/

<!-- TOC -->

- [Introduction](#introduction)
- [Browser support for Web Workers](#browser-support-for-web-workers)
- [Create a Web Worker](#create-a-web-worker)
- [Communication with a Web Worker](#communication-with-a-web-worker)
  - [Using postMessage in the Web Worker object](#using-postmessage-in-the-web-worker-object)
    - [Send back messages](#send-back-messages)
    - [Multiple event listeners](#multiple-event-listeners)
  - [Using the Channel Messaging API](#using-the-channel-messaging-api)
- [Web Worker Lifecycle](#web-worker-lifecycle)
- [Loading libraries in a Web Worker](#loading-libraries-in-a-web-worker)
- [APIs available in Web Workers](#apis-available-in-web-workers)

<!-- /TOC -->

## Introduction

A Web Worker runs JavaScript in a background thread, separate from the page's main thread.

Use a worker for CPU-heavy work that would otherwise make the page slow or unresponsive.

The main thread still handles the DOM and user interface. The worker cannot access `window` or `document`, so both sides communicate through messages.

This guide focuses on **dedicated workers**, which belong to one page or script. The [MDN Web Workers guide](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) also covers shared workers.

They have quite a few limitations:

- no direct access to the [DOM](https://flaviocopes.com/dom/)
- the initial worker script normally needs to be same-origin
- the page and worker don't share ordinary JavaScript objects
- behavior for pages loaded with `file://` is not consistent, so use a local web server

The global scope inside a worker is a [`WorkerGlobalScope`](https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope), not `Window`.

## Browser support for Web Workers

Dedicated Web Workers are widely available in current browsers.

You can check for Web Workers support using

```js
if ('Worker' in window) {
  // Web Workers are available
}
```

## Create a Web Worker

Create a classic worker by passing its script URL to the `Worker` constructor:

```js
const worker = new Worker('worker.js')
```

You can also create a module worker:

```js
const worker = new Worker('worker.js', {
  type: 'module'
})
```

Module workers support `import` statements. The [MDN Worker constructor reference](https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker) explains URL and same-origin rules.

## Communication with a Web Worker

There are two main ways to communicate to a Web Worker:

- the postMessage API offered by the Web Worker object
- the [Channel Messaging API](https://flaviocopes.com/channel-messaging-api/)

### Using postMessage in the Web Worker object

You can send messages using `postMessage` on the `Worker` object.

By default, message data is copied using the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). It is not shared.

Some objects, such as `ArrayBuffer`, can be transferred instead. A transferred object is no longer usable by the sender.

> main.js

```js
const worker = new Worker('worker.js')
worker.postMessage('hello')
```

> worker.js

```js
self.onmessage = event => {
  console.log(event.data)
}

self.onerror = event => {
  console.error(event.message)
}
```

#### Send back messages

A worker can send messages back to its creator using its global `postMessage()` function:

> worker.js

```js
self.onmessage = event => {
  console.log(event.data)
  self.postMessage('hey')
}

self.onerror = event => {
  console.error(event.message)
}
```

> main.js

```js
const worker = new Worker('worker.js')
worker.postMessage('hello')

worker.onmessage = event => {
  console.log(event.data)
}
```

#### Multiple event listeners

If you want to setup multiple listeners for the `message` event, instead of using `onmessage` create an event listener (applies to the `error` event as well):

> worker.js

```js
self.addEventListener('message', event => {
  console.log(event.data)
  self.postMessage('hey')
})

self.addEventListener('message', () => {
  console.log(`I'm curious and I'm listening too`)
})

self.addEventListener('error', event => {
  console.log(event.message)
})
```

> main.js

```js
const worker = new Worker('worker.js')
worker.postMessage('hello')

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

### Using the Channel Messaging API

Instead of using the built-in postMessage API offered by Web Workers, we can choose to use the more general-purpose [Channel Messaging API](https://flaviocopes.com/channel-messaging-api/) to communicate to them.

> main.js

```js
const worker = new Worker('worker.js')
const channel = new MessageChannel()

channel.port1.addEventListener('message', event => {
  console.log(event.data)
})
channel.port1.start()

worker.postMessage(
  { port: channel.port2 },
  [channel.port2]
)
```

> worker.js

```js
self.addEventListener('message', event => {
  const port = event.data.port
  port.postMessage('hello from the worker')
})
```

The `MessagePort` is listed as a transferable object. Ownership moves to the worker when it is posted.

## Web Worker Lifecycle

A dedicated worker can receive messages after its initial script finishes. Don't rely on a worker stopping merely because the current function returned.

Stop it explicitly with `terminate()` from the main thread:

> main.js

```js
const worker = new Worker('worker.js')
worker.terminate()
```

`terminate()` stops the worker immediately. It does not give the worker time to finish.

Inside the worker, call `close()` to stop accepting new tasks:

```js
self.onmessage = event => {
  console.log(event.data)
  self.close()
}
```

The HTML specification defines the details of [worker lifetime and termination](https://html.spec.whatwg.org/multipage/workers.html#the-worker's-lifetime).

## Loading libraries in a Web Worker

Classic workers can load scripts synchronously with `importScripts()`:

```js
importScripts('../utils/file.js', './something.js')
```

Module workers cannot use `importScripts()`. Use normal static or dynamic imports instead:

```js
import { calculate } from './calculate.js'
```

See the [MDN `importScripts()` reference](https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/importScripts) for the security and cross-origin rules.

## APIs available in Web Workers

The DOM is not available in a Web Worker, so you cannot interact with `window` or `document`.

Many other APIs are available, including:

- the [Fetch API](https://flaviocopes.com/fetch-api/)
- [IndexedDB](https://flaviocopes.com/indexeddb/)
- [Promises](https://flaviocopes.com/javascript-promises)
- the [Channel Messaging API](https://flaviocopes.com/channel-messaging-api)
- the [Cache API](https://flaviocopes.com/cache-api/)
- the [Console API](https://flaviocopes.com/console-api/)
- timers such as `setTimeout()` and `setInterval()`
- [WebSockets](https://flaviocopes.com/websockets/)
- `crypto`
- `OffscreenCanvas`

Support still varies by API and browser. Check the official [list of functions and classes available to workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Functions_and_classes_available_to_workers) before relying on one.
