Introduction to Javascript Web Workers and threading

Published

The Web and his friend Javascript are a strange bunch that has only grown larger and larger ever since their inception, finally becoming the “standard” way to create Desktop and Mobile applications.

While Javascript has progressively improved over the last few years, some of its warts still show their ugly faces. Chief among which is performance :tm:, a multi-faceted issue that probably won’t go anywhere for a while.

This post is meant as a part of a series about Javascript (and the now infamous Typescript), performance issues and providing a better experience for users, cross-platform.

What are Web Workers?

To understand the appeal of Web Workers, we’ll need to dabble a little into Javascript’s specifities.

Javascript was primarily meant as a way to add some interactivity to web pages and I’m pretty sure no one back in 1995 had any idea today’s websites would grow to be so complex. The language was also created on quite a short notice (only ten days) which meant some design decision had to be made for simplicity.

Add some security concerns, obvious concurrency issues and you get the single threaded language that is Javascript. Javascript is an interpreted language which means that one and only one statement can be processed at a time. (I’m not going to be writing about promises / async await & the event loop in this post)

In practical terms, this means that a long running statement (think a function that takes a lot of time, maybe some image processing, number crunching…) will prevent the rest of your script from executing until it is done. This also usually turns your web page unresponsive if in a browser environment. since everything runs on the same thread, which we’ll call the main thread.

A prevalent solution for other languages is to create or spawn a new thread that would be responsible for handling this long running script, messaging back the main thread once done with the task.

But for a few years now, web browsers have implemented web workers and while they are not quite equivalent to threads, they bring the same benefits.

Web Workers can be thought of a background threads which can deal with input / output, network requests and most of your JS needs. One obvious exception is manipulating the DOM. This can only be done from the main thread which is sometimes called the UI thread.

A special note for our mobile developer friends: React Native has its own set of issues regarding threading and performance that PWA do not share.

Good thing to take note of, web workers have close to complete support among browsers.

How can I use them?

Now that we know why we would need Web Workers, let’s try them out.

I’ve already setup a complete github repository with a working implementation.

Using Web Workers is pretty simple:

  • First, you’ll need to put your long running function in a separate file. (In my github repo, that’s the myworker.js)
    • If you want to use Typescript, you’ll have to setup compiling for it in your buildchain and put it in your public folder.
  • Second, you’ll need to setup the messaging between the threads.

You’ve got mail

In Javascript, threads are completely separate processes, meaning communication between them is fairly restricted.

In order to send data from one thread to another, you’ll need to send a message. Thankfully this is quite straightforward.

// index.js

// We'll start by creating the worker. The path to your worker is relative to your server.
const worker = new Worker("/myworker.js");

// We'll send him a message
worker.postMessage("Hello worker");

// What happens when our worker answers
worker.onmessage = (event) => {
    const data = event.data;
    doWhateverWithData(data);
}

doWhateverWithData(data) {
    console.log(`I received data: $data`);
}

/*
	Here's the event interface from typescript

interface MessageEvent<T = any> extends Event {
    // Returns the data of the message.
    readonly data: T;
    
    // Returns the last event ID string, for server-sent events.
    readonly lastEventId: string;
    
    // Returns the origin of the message, for server-sent events and cross-document messaging.
    readonly origin: string;
    
    // Returns the MessagePort array sent with the message, for cross-document messaging and channel messaging.
    readonly ports: ReadonlyArray<MessagePort>;
    
    // Returns the WindowProxy of the source window, for cross-document messaging, and the MessagePort being attached, in the connect event fired at SharedWorkerGlobalScope objects.
    readonly source: MessageEventSource | null;
}
*/

And then let’s take a look at our worker file.

// What happens when we receive a message
onmessage = (event) => {
    const data = event.data;
    const transformedData = myLongFunction(data);
    
    postMessage(transformedData);
}

function myLongFunction(input){
    // Do something that takes a long time
    const transformed = ...;
    return  transformed; 
}

If you are following along, please be careful: there’s a capital M for postMessage but not for onmessage.

And you’re done! Your message will go from your main thread to your background thread with the data and will be notified when done.

As you saw, this is pretty simple. You’ll have to deal with error handling and such but from here on out, it’s all standard javascript.

Once again, if you want to see a simple working implementation, head on over to my github repo.