Off the Main Thread with Web Workers

clock icon June 29, 2019

Image result for main thread sew

image credit: Maile Proctor


In my early years of web development, I remember seeing the error below quite frequently:


At that time, I didn't know what a thread was (maybe I still don't know what it is totally) but it got me curious enough to take a note of it. These days I've been digging into the web platform to try out things I have not done previously and in this article I'll be looking into web workers.


Web Workers

Javascript is single-threaded, meaning it can only execute one line at a time so if a particular task takes too long to execute, other tasks have to wait until it’s completion. While the browser executes Javascript, the screen remains unresponsive. Any user interaction at that time results in nothing which leaves users frustrated or worse, leaving the website. Hence the need for web workers.

A web worker is a script that runs in the background and does not interfere with the main thread. Long-running or CPU intensive tasks can be done in a worker and the main thread notified on completion. The API for the worker is quite simple.


To create a new worker:

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

The Worker() constructor takes an argument which is a script file containing the code to be run. A message can be sent to

the worker like so:

worker.postMessage('This message is from the main thread');

In the worker, you can respond when a message is received like so:

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

The message can be accessed from the event's data property. A worker can also send a message to the script that created it like so:

postMessage('This message is from the web worker');

The message can be received in the main script by listening to the onmessage event:

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

Errors can be handled in the onerror handler

worker.onerror = (e) => {
  // handle error
};

When a worker is no longer in use, it should be terminated.

worker.terminate();


Types of Web Workers

There are two types of Web workers namely:

  • Dedicated Worker
  • Shared Worker


The previous examples are of dedicated workers, which can only be accessed by the script that created it. A shared worker, on the other hand, is accessible by multiple scripts. The API is closely the same with dedicated workers with a few noticeable differences:


Creating a Shared Worker

const sharedWorker = new SharedWorker('my-shared-worker.js');

The first noticeable difference is in the constructor name. A shared worker is created using the SharedWorker() constructor. Communication between shared workers and scripts are done via a port - an explicit port is opened for the worker to communicate with scripts. This is done implicitly in dedicated workers.


The port is created implicitly using the worker's onmessage event handler or explicitly using the start() method.

// implicitly
sharedWorker.onmessage = (e) => { ... };

// explicitly
sharedWorker.start();


Sending messages to and from Shared Workers

Just like dedicated workers, the postMessage() method is used. In the case of shared workers, it is invoked through a port like so:

sharedWorker.port.postMessage('Hello to shared worker');

In the worker file, messages are received in the onconnect handler:

onconnect = (e) {
  const port = e.ports[0];

  port.onmessage = (e) => {
    console.log(e.data);
    port.postMessage('Hello from shared worker');
  };
};

The port is gotten from the event object and subsequent communication is done over the port. In the main script, messages can be received from the shared worker like so:

sharedWorker.port.onmessage = (e) => {
  console.log(e.data);
};


A few things of note

  • Workers can't directly manipulate the DOM or access some window properties or methods which makes sense as they run in a separate thread. This makes them suitable for API calls, intensive computations like manipulating image/video data, and generally things that could take a considerable amount of time.
  • Workers can import scripts using the importScripts() global method. This could be 3rd party libraries or your own libraries.


How is all this useful in an actual project? I built a web app with Web Workers and the Speech Synthesis API. It’s a pdf reader i.e. you select a pdf on your device and it reads the contents of the pdf to you. It’s also a PWA so it works fully offline and can be added to the home screen. You can learn more about how I built this in my article on the Speech Synthesis API.


Useful Links