Dedicated Workers

Dedicated workers are only accessible from the code that creates them.

Creating Workers

We create workers by calling the Worker constructor.

let myWorker = new Worker('worker.js');

Message communication between worker and its parent

Workers are communicated with their parent via the so-called message passing. More specifically, they are implemented via postMessage and onmessage in JavaScript.

Here is the code on the parent thread:

myWorker.postMessage("send to worker");
myWorker.onmessage = function(e) {
  let result = `message from worker: ${e.data}}`;
  console.log(result);
}

postMessage sends a message to the worker; then, onmessage registers an event listener to receive a message from the worker.

Then, here is the code on the worker thread:

postMessage("send to parent");
onmessage = function (e) {
    let result = `message from main thread: ${e.data}`;
    console.log(result);
}

A simple example that adds two numbers

We start from the main thread code, which supplies two numbers to the worker.

let myWorker = new Worker('worker.js');

let num1 = 5;
let num2 = 6;

myWorker.postMessage([num1, num2]);

myWorker.onmessage = function(e) {
  document.querySelector('#app').innerText = `Message from Worker: ${e.data}`
}

Then, the worker side accepts the two numbers, adds them, and sends the result back to the main thread.

self.onmessage = function(e) {
    console.log('Message received from main script');
    let sum = e.data[0] + e.data[1];
    let workerResult = `Result: ${sum}`;
    console.log('Posting message back to main script');
    postMessage(workerResult);
  }

Object transfer vs. copy in postMessage

By default, objects are copied from the parent thread to the worker during postMessage. Note that there are several properties that are not copied, such as __proto__ and constructor.

There is another way in postMessage, which is to transfer the ownership of objects from the parent to the worker. After the transfer, the parent thread does not have access to the objects anymore. Here is the syntax:

myWorker.postMessage(message, [transfer]);

Let us start from an example with copying an object. Here is the code on the main thread:

let myWorker = new Worker('worker.js');

const buffer = new ArrayBuffer(8);
const view = new Int32Array(buffer);

view[0] = 5;
view[1] = 6;

myWorker.postMessage(buffer);

try{
   console.log("View:" + view);
}
catch (e) {
  console.log(e.message);
}

myWorker.onmessage = function(e) {
  document.querySelector('#app').innerText = `Message from Worker: ${e.data}`
}

And here is the code on the worker:

self.onmessage = function(e) {
    console.log('Message received from main script');
    const view = new Int32Array(e.data);
    let sum = view[0] + view[1];
    let workerResult = `Result: ${sum}`;
    console.log('Posting message back to main script');
    postMessage(workerResult);
  }
myWorker.postMessage(message, [transfer]);

Now let us change the code on the main thread to transferring an object and compare the output.

let myWorker = new Worker('worker.js');

const buffer = new ArrayBuffer(8);
const view = new Int32Array(buffer);

view[0] = 5;
view[1] = 6;

- myWorker.postMessage(buffer);
+ myWorker.postMessage(buffer, [buffer]);

try{
   console.log("View:" + view);
}
catch (e) {
  console.log(e.message);
}

myWorker.onmessage = function(e) {
  document.querySelector('#app').innerText = `Message from Worker: ${e.data}`
}

The advantage of transferring an object is to save the time used in copying especially when the object is large.

Note that only transferrable objects, such as ArrayBuffer, can be transferred between threads. We cannot transfer, but only copy, other objects, such as object literals.

importScripts

importScripts of WorkerGlobalScope allows the web worker to import an external scripts into the worker's scope.

importScripts(a.js);