Web Workers
By Flavio Copes
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.
- Introduction
- Browser support for Web Workers
- Create a Web Worker
- Communication with a Web Worker
- Web Worker Lifecycle
- Loading libraries in a Web Worker
- APIs available in Web Workers
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 also covers shared workers.
They have quite a few limitations:
- no direct access to the 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, not Window.
Browser support for Web Workers
Dedicated Web Workers are widely available in current browsers.
You can check for Web Workers support using
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:
const worker = new Worker('worker.js')
You can also create a module worker:
const worker = new Worker('worker.js', {
type: 'module'
})
Module workers support import statements. The MDN Worker constructor reference 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
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. 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
const worker = new Worker('worker.js')
worker.postMessage('hello')
worker.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
self.onmessage = event => {
console.log(event.data)
self.postMessage('hey')
}
self.onerror = event => {
console.error(event.message)
}
main.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
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
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 to communicate to them.
main.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
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
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:
self.onmessage = event => {
console.log(event.data)
self.close()
}
The HTML specification defines the details of worker lifetime and termination.
Loading libraries in a Web Worker
Classic workers can load scripts synchronously with importScripts():
importScripts('../utils/file.js', './something.js')
Module workers cannot use importScripts(). Use normal static or dynamic imports instead:
import { calculate } from './calculate.js'
See the MDN importScripts() reference 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
- IndexedDB
- Promises
- the Channel Messaging API
- the Cache API
- the Console API
- timers such as
setTimeout()andsetInterval() - WebSockets
cryptoOffscreenCanvas
Support still varies by API and browser. Check the official list of functions and classes available to workers before relying on one.
Related posts about platform: