Web Workers

Introduction

JavaScript is single threaded. Nothing can run in parallel at the same time.

This is great because we don’t need to worry about a whole set of issues that would happen with concurrent programming.

With this limitation, JavaScript code is forced to be efficient from the start, otherwise the user would have a bad experience. Expensive operations should be asynchronous to avoid blocking the thread.

As the needs of JavaScript applications grew, this started to become a problem in some scenarios.

Web Workers introduce the possibility of parallel execution inside the browser.

They have quite a few limitations:

  • no access to the DOM: the Window object and the Document object
  • they can communicate back with the main JavaScript program using messaging
  • they need to be loaded from the same origin (domain, port and protocol)
  • they don’t work if you serve the page using the file protocol (file://)

The global scope of a Web Worker, instead of Window which is in the main thread, is a WorkerGlobalScope object.

Browser support for Web Workers

Pretty good!

You can check for Web Workers support using

if (typeof Worker !== 'undefined') {

}

Create a Web Worker

You create a Web Worker by initializing a Worker object, loading a JavaScript file from the same origin:

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

Communication with a Web Worker

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

Using postMessage in the Web Worker object

You can send messages using postMessage on the Worker object.

Important: a message is transferred, not shared.

main.js

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

worker.js

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

worker.onerror = (e) => {
  console.error(e.message)
}

Send back messages

A worker can send back messages to the function that created it. using worker.pushMessage():

worker.js

worker.onmessage = (e) => {
  console.log(e.data)
  worker.pushMessage('hey')
}

worker.onerror = (e) => {
  console.error(e.message)
}

main.js

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

worker.onmessage = (e) => {
  console.log(e.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

worker.addEventListener('message', (e) => {
  console.log(e.data)
  worker.pushMessage('hey')
}, false)

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

worker.addEventListener('error', (e) => {
  console.log(e.message)
}, false)

main.js

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

worker.addEventListener('message', (e) => {
  console.log(e.data)
}, false)

Using the Channel Messaging API

Since Workers are isolated from the main thread, they have their own, to communicate to them we need a special API: the Channel Messaging API.

main.js

const worker = new Worker('worker.js')
const messageChannel = new MessageChannel();
messageChannel.port1.addEventListener('message', (e) => {
  console.log(e.data)
})
worker.postMessage(data, [messageChannel.port2])

worker.js

self.addEventListener('message', (e) => {
  console.log(e.data)
})

A Web Worker can send messages back by posting a message to messageChannel.port2, like this:

self.addEventListener('message', (event) => {
  event.ports[0].postMessage(data)
})

Web Worker Lifecycle

Web Workers are launched and if they do not stay in listening mode for messages through worker.onmessage or by adding an event listener, they will be shut down as soon as their code is run through completion.

A Web Worker can be stopped using its terminate() method from the main thread, and inside the worker itself using the global method close():

main.js

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

worker.js

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

worker.onerror = (e) => {
  console.error(e.message)
}

Loading libraries in a Web Worker

Web Workers can use the importScripts() global function defined in their global scope:

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

APIs available in Web Workers

As said before, the DOM is not reachable by a Web Worker, so you cannot interact with the window and document objects. Also parent is unavailable.

You can however use many other APIs, which include: