Web Workers for Everyone

Web Workers for Everyone

There are at least 10s of meaningful articles about web workers on the internet. There are good articles, and there are other articles, but after attempting to use several of them to get a simple worker up and running, I ended up using a Stack Overflow article that I can't even find now.

It is not that web workers are difficult to use or understand, but most of the other articles you'll find like a compelling use case or a simple method for getting a first worker up and running, so you can fiddle with it for 10 minutes and add it to your resume. If that is your goal, I'm here for you, in fact, lets just skip to the part where you head over to my GitHub repo and I never see or hear from you again.

jdcauley/basic-web-worker-tooling
Contribute to jdcauley/basic-web-worker-tooling development by creating an account on GitHub.

For the 7 of you who continued reading after I provided the link above, I'm going to break this down into simple parts. FYI there are a few things missing from that example at the moment, but only you 4 get to know that.

A Little Background

Lets start with a couple of "basics".

Using Web Workers provides you the web developer the ability to run additional processes on background threads. In the world of Page Speed Insights, Lighthouse and Web Vitals moving processes off the main thread has become a way to stay employed as the team "performance expert".

Web Workers are the browser API that provides you, the resident performance expert, access to use secondary threads for which ever process you have that can run in that context.

So what can you run in that context?

Good question, for now, not as much as you'd like. Nothing that requires the DOM (until this gets stable enough). But you can definitely do math, which is everyone's favorite thing to do with JavaScript.

The example provided above is doing date math, with Moment.js.

The Files in Question

In our little example we have our primary JavaScript file and our Worker file.

Modern web development requires a lot pretty complicated tooling, so this project also includes Parcel.js tooling to build the primary and worker JavaScript files.

There are many ways to manage your worker scripts so that you can load them from the primary thread, but if you want to jump in a find out what you can do, the example repo will at least prevent you from spending too much time with tools.

At the moment this repo will not run properly inside CodeSandbox (I'll keep working on it)

For the purpose of our discussion we have two files, src/index.js and src/worker.js

Let's break down the parts of src/index.js first (for our purposes, I'm only going to cover the implementation that includes a promise):

const momentWorker = new Worker(`/worker.js`);
Loading the Worker from the Primary Thread.

We're calling up our worker, using the browsers Worker API, the first argument being the URL of our Worker. You can also create a blob worker which is really cool, but will get it's own post.

Once we have our Worker we can start putting it to use. Because the Worker runs on its own thread, it should always be treated as an asynchronous function, which means using promises.

In order for our primary thread code to work we should hope over to src/worker.js and see what we're doing over there, this is the entire contents of my worker file.

importScripts("https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js")

const relativeTime = timestamp => {
  return moment.unix(timestamp).fromNow();
}

self.addEventListener("message", e => {
  const prettyTime = relativeTime(e.data[0]);
  self.postMessage(prettyTime);
})
Contents of our Worker File (loaded vie 'new Worker')

Well, first up, we're importing moment.js via importScripts,  which will give us access to the Moment.js library inside our worker.

You might have notice that rather than using Document or even Window, we're adding a listener with self, Workers have a limited "scope" and don't have access to all the Window and Document APIs, you are likely used too, so we'll make do.

If you've made it this far into my rambling tutorial, you're probably wondering, how the fuck do I get data to and from the Worker thread? I'm glad you asked.

You have to use postMessage.

If you're anything like me, and frankly probably a ton of other web developers, you have probably never used the postMessage API, and since I don't have a post on that yet, you're going to have to trust me or go read MDN.

So we add an event listener to listen for messages pointed at our Worker, hand the data off to the relativeTime function and then sling our response back to the main thread by posting another message from the Worker.

Let's be honest, this is a little tedious, but, shit has potential.

Back from Work

Well, dear reader, and I'm using the singular intentionally, there is no way more than one person (if that) will have made it this far, we're finally ready to put our Worker. to. work... (record setting punctuation in a run-on sentence, right fucking here)

Once, we've written our worker, and loaded it into our main thread for use we have below the use of that worker. I've abstracted the worker into a simple promise that can be easily reused.

Our whole worker converts a Unix timestamp into a relative time, which we can feed back out to a hypothetical UI to make a Product Manager (like myself) self satisfied that they are going to deliver a better experience to the user.

const getRelativeTimePromise = timestamp => {
  momentWorker.postMessage([timestamp]);

  return new Promise((resolve, reject) => {
    momentWorker.addEventListener(
      "message",
      evt => {
        resolve(evt.data);
        if (!evt.data.ok) {
          reject(evt.data);
        }
      },
      false
    );
  });
};

getRelativeTimePromise(1529046600)
  .then(result => {
    console.log(result);
  })
  .catch(err => {
    console.log(err);
  });

But was it worth it?

Meh, maybe?

This is a super super simple and maybe kind of terrible example.

Handling relative times has been made much simpler by newer browser APIs but the theory is sound.

Computationally "expensive" tasks can be removed from the main thread to allow for other operations that can't be handled by a Worker.

Keep DOM tasks on the main thread and everything else in a worker. I'm sure at some point someone will coin "Worker First Dev" and then everyone will agree we definitely adhere to that practice at this company, in the meantime, this your chance to cement your future as a Performance Expert.

Good Luck.