The JavaScript engine

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.

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

A long computation on the main thread freezes the page. A worker gives it its own thread.

The price is isolation. A worker can’t touch window or document. The two sides talk only through messages.

This lesson covers dedicated workers, which belong to one page. The MDN Web Workers guide also covers shared workers. The limits:

  • no direct access to the DOM
  • the worker script normally needs to be same-origin
  • the page and worker don’t share JavaScript objects
  • file:// pages behave inconsistently, so use a local web server

The global scope inside a worker is a WorkerGlobalScope, not Window. That’s why worker code says self.

Browser support

Every current browser has them. To check anyway:

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

Create a Web Worker

Pass the script URL to the Worker constructor:

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

For import statements inside the worker, create a module worker:

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

The MDN Worker constructor reference explains the URL rules.

Communicate with the worker

Two ways:

Using postMessage

The page sends, the worker receives in onmessage:

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)
}

The data is copied with the structured clone algorithm, not shared. An ArrayBuffer can be transferred instead, after which the sender can’t use it.

Send messages back

The worker replies with its global postMessage():

worker.js

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

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

The page listens with worker.onmessage:

main.js

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

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

Multiple event listeners

onmessage holds one function. For more, use addEventListener(), same for error:

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

The Channel Messaging API gives you a channel with two ports. Create it in the page and hand one port to the worker:

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')
})

Notice the second argument to postMessage(): a MessagePort is transferable, so ownership moves to the worker.

Web Worker lifecycle

A worker keeps running after its script finishes, waiting for messages. Stop it from the page with terminate():

main.js

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

It stops immediately, with no time to finish. From inside, call close():

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

The HTML spec covers worker lifetime and termination.

Loading libraries in a Web Worker

A classic worker loads scripts synchronously with importScripts():

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

A module worker uses normal imports instead:

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

See the MDN importScripts() reference for the cross-origin rules.

APIs available in Web Workers

No DOM, but you still get a lot:

Check MDN’s list of functions and classes available to workers before relying on one.

Lesson completed