Skip to main content

Workers

Workers are ways of offloading JavaScript tasks onto a separate thread. There are three different types of workers:

  • dedicated workers: Heavy-set workers that work in a separate JS thread, meant for doing work that shouldn't run on main thread
  • shared workers: Like dedicated workers but can be shared across different contexts
  • service workers: act as a proxy for all network requests for the app

Dedicated workers

Dedicated Worker basics

Workers are a way of multithreading in JavaScript, where you can delegate expensive tasks to be performed by a worker, which works in the background much like a service worker.

You can then communicate with workers using a messaging and event based system.

  1. Register the worker. Create a worker.ts file. The global object there is self.

    const worker = new Worker("./worker.ts");

You can also listen to errors on the worker like so:

worker.onerror = function (e) {
console.log("error in worker", e);
e.preventDefault();
};

If you want to prevent the error in a worker from propagating and crashing your program, you can call e.preventDefault() to prevent the error from propagating into the window and main thread.

Now let's talk about sending messages between the worker and main thread.

TypeScript

The self object in a dedicated worker file is of type DedicatedWorkerGlobalScope.

You can enable the type intellisense for a dedicated worker in the tsconfig:

{
"compilerOptions": {
"lib": ["webworker", "es2015"]
}
}

Worker lifecycle

The worker lifecycle goes through three stages:

  • initializing:
  • active
  • terminated

Here are the different methods a worker has to deal with the lifecycle:

  • worker.terminate(): terminates the worker
  • worker.close(): discards all worker tasks in the event loop

TIP

Both worker.terminate() and worker.close() are idempotent operations, meaning you can call them multiple times without any harm.

Sending messages

From main thread to worker

main.ts
worker.postMessage(payload);

The worker.postMessage() method takes a single argument of any type, which is the payload to be sent to the worker.

You can then listen to messages sent by the main thread in the worker by listening to the "message" event listener, which passes in an event object with the data property, where event.data is the payload that the message posted in the first place.

worker.ts
self.addEventListener("message", (event) {
const data = event.data;
});

From worker to main thread

It's pretty much the same API to post messages and listen to them, except we use self.postMessage() in the worker thread and worker.onmessage in the main thread.

worker.ts
self.postMessage(payload);

Whatever you pass into the postMessage() method gets sent as the event.data property in the message listener.

main.ts
worker.addEventListener("message", (event) =>{
const data = event.data;
});

Worker class

Here is an implementation of a worker class that makes it easier to work with workers.

// message typing, where we use keys as message strings, and values as the payload
interface Messages {
terminate: {
type: "terminate";
payload: undefined;
};
countToACertainNumber: {
type: "countToACertainNumber";
payload: number;
};
}

export class WorkerClass {
// public access to worker so you can do worker specific things like worker.terminate()
constructor(public worker: Worker) {}

onError(callback: (err: ErrorEvent) => void) {
this.worker.onerror = callback;
}

postMessage<T extends keyof Messages>(
message: T,
payload: Messages[T]["payload"]
) {
// automatically add "type" property to message we are sending
this.worker.postMessage({
type: message,
payload,
});
}

// listen for a specific message
onMessage<T extends keyof Messages>(
message: T,
// add callback type which takes in payload of the specific message we are listening on
callback: (payload: Messages[T]["payload"]) => void
) {
this.worker.onmessage = function (event) {
const data = event.data as Messages[T];

if (data.type === message) {
// pass payload into callback
callback(data.payload);
}
};
}
}

We can then use it like this:

// main thread

function countToACertainNumber(num: number) {
const worker = new WorkerClass(new Worker("./worker.ts"));

// onError listener
worker.onError((e) => {
console.log("error in worker", e);
});

// send a message
worker.postMessage("countToACertainNumber", num);

// listen for the "terminate" message being sent from the worker
worker.onMessage("terminate", () => {
// public access on worker, terminate worker
worker.worker.terminate();
});
}

countToACertainNumber(100);

And then in the worker thread:

import { WorkerClass } from "./WorkerClass";

// create worker abstraction from self object
const worker = new WorkerClass(self as unknown as Worker);

// listening to message coming from main thread
worker.onMessage("countToACertainNumber", (num) => {
// execute code on worker thread
let count = 0;
while (count < num) {
console.log(count++);
}
// send back message telling to terminate
worker.postMessage("terminate", undefined);
});