JG logo

Joshua Gilless

Hello all, today I’m taking a look at web workers. The general idea is that I’d like to implement a tiny one that just sends a message back and forth between the main index.js and the worker in worker.js. I’m hopeful that I can show you how I explore something that I don’t really know anything about. After we implement a tiny one, we’ll look at further use cases for “real” ones.

I start a lot of my projects the same way, just a HTML file, a JavaScript file, and a CSS file. For this project, we’re not even going to need a CSS file.

I have several folders at my root, personal stuff goes in a folder called “projects”. When I open my terminal, I often run a few commands.

Here they are, for brevity’s sake, concatenated:

cd projects/tmp && mkdir webworkers && cd webworkers && touch index.html index.js && echo "console.log('hello');" > index.js

It changes me to a temporary directory, where I keep a lot of little silly things that I haven’t build a project out of and can delete at any time. Any time a project is in this tmp folder, it is not worth caring about. If I start to care about the project, I move it up a directory.

The commands make a folder called webworkers and then adds two files, index.html and index.js. The file index.js starts off with a console.log('hello'); so that I can just verify that it’s all connected when I first open it all up.

I use VSCode as my editor. VSCode comes with the Emmet toolkit that lets you just type html:5, hit tab, and it outputs a boilerplate html document.

Before the closing body tag, I manually typed:

<script src="index.js"></script>

And now I’ve got a very basic static site.

Simple Web Server

I use a python module to run a static web server in a directory. If you have python 2 run (in your terminal):

python -m SimpleHTTPServer 8000

If you’re using python 3 run:

python -m http.server 8000

If you’re lazy, and you do this a lot, you can also add an alias to your .bash_profile or whatever file your shell executes on load.

alias pserve="python -m http.server 8000"

Once your web server is started, open your browser, navigate to localhost:8000 and open the console. If you’ve done everything right you should see a “hello” waiting for you.

Web Workers!

Ok let’s read a little documentation. So it looks like we want to create a worker from a separate file and that worker will run on a background thread. Cool, let’s make the file.

touch worker.js

It has an event handler called onmessage, so we can make our whole file just that handler for now.

worker.js:

onmessage = function(e) {
  console.log(e);
};

I should explain what I’m doing here. We can use onmessage directly here because in the worker context, self and this both point to the global worker scope. The scoping is very similar to how you may use something like fetch or console directly in the main thread, even though they’re on the window object because the main thread’s global scope is Window.

If you’re feeling verbose, you could write this instead:

self.addEventListener("message", function(e) {
  self.console.log(e);
});

The advantage of using addEventListener is that you don’t override existing listeners on the event. If you use the sort-of-inline onmessage, you only ever get one event listener at a time, and if you ever declare a new onmessage handler, you override the first one. With addEventListener you can add as many as you want, but you have to worry about discarding the listeners when you’re done with them. It’s a small tradeoff to be aware of.

It’s probably overall better to just stay consistent with addEventListener. And if I’m using addEventListener, it means I should really separate my function out in case I need to remove the listener later on. Saying out loud as I do this: “Premature optimization is the root of all evil, but building good habits is not.”

function handleMessage(e) {
  console.log(e);
}
addEventListener("message", handleMessage);

Ok, so that’s our worker.js, let’s create it and send a message.

index.js:

const worker = new Worker("worker.js");

worker.postMessage("Hello");

Save and refresh the page and see what happens in the console.

> MessageEvent {isTrusted: true, data: “Hello”, …}

Very nice! There are a lot of properties on this MessageEvent object. Let’s take a closer look at data, since I see that has “Hello” in it, and means data is what we sent.

Let’s try posting an object! The HTML specification says attribute any data, so it can be more than a string.

index.js:

const worker = new Worker("worker.js");

worker.postMessage({ your: "face", is: "cool" });

Change what we’re logging in worker.js to be just the data property of the event:

function handleMessage(e) {
  console.log(e.data);
}
addEventListener("message", handleMessage);

> {your: “face”, is: “cool”}

Well, that’s what I wanted to see. Now we know how to post data to the worker, what about receiving it and bringing it back?

Let’s go back to index.js and have our worker receive a message:

const worker = new Worker("worker.js");

worker.postMessage({ your: "face", is: "cool" });

function handleMessage(e) {
  console.log(e.data);
}

worker.addEventListener("message", handleMessage);

And let’s have our worker.js send a message when it receives one.

function handleMessage(e) {
  if (e.data.is === "cool") {
    postMessage("yes");
  } else {
    postMessage("how dare you?");
  }
}
addEventListener("message", handleMessage);

Save and refresh:

> yes

Excellent.

Let’s change index.js one more time, to make sure all the data can be evaluated:

const worker = new Worker("worker.js");

worker.postMessage({ your: "face", is: "not cool" });

function handleMessage(e) {
  console.log(e.data);
}

worker.addEventListener("message", handleMessage);

Save, run again:

> how dare you?

There you have it, we can send messages with data back and forth from the index to the worker and back.

Tidying Up

You can remove the event listeners when you’re done with them, both on the worker object in the main thread, and on the global scope in the worker thread.

Main thread:

worker.removeEventListener("message", handleMessage);

Worker thread:

self.removeEventListener("message", handleMessage);

That’s an important thing to care about. Your code is probably running somewhere on less expensive hardware than your 2014 MacBook Pro that you’re still holding on to because you don’t want one of the new terrible keyboards.

Additionally, you might want to kill the worker thread when you’re done with it. No use keeping it around after it’s done. This can also be done in the main thread, or in the worker thread.

In the main thread, it looks like this:

worker.terminate();

And in the worker thread, it looks like this:

self.close();

The Browser Check

Ok, one last thing. Almost all browsers support Web Workers, but Opera Mini exists and people still use IE6 for whatever reason. You can verify that the browser trying to run your code supports web workers with a simple check:

if (window.Worker) {
  ...
}

Use Cases

Imagination is key here. This gets you some actual multithreading capabilities in the browser. It’s not a JavaScript feature, JavaScript is single-threaded. This is an HTML feature.

What if you put your apps state management into a background worker? You could have all your expensive state calculations run in the background and have the main thread to do all the UI work. That would be pretty sweet.

What if you put all your data fetching in a worker? Your main thread wouldn’t have to block anything while you’re waiting on a response from the server.

What if you were building some kind of data-heavy app and wanted people to be able to upload a CSV and then parse that data into your application without having to send it to a server? A Web Worker could really help with offloading file processing off of the main thread.

Conclusion

I think this is the simplest worker we can make. It turns out that after what we’ve done here, it’s just writing more javascript.

Each worker has a global context, but can’t access the DOM, window, or document. You CAN access a few things that you would think of as under the normal window context that are also under the worker context.

There’s a lot more to explore here. I’ve built a tiny example, just a proof of concept that says “Yep, this thing works.” The next thing to do would be to build something non-trivial, like that thing that parses a CSV file without sending it to a server.

I think that exploring things you don’t understand is really important. In this case, I learned how easy web workers are to set up and use. Any time you learn something small like this, it compounds on your ability to make good decisions when you’re building things later on.

I hope you picked up something cool from this blog post. If you’re using web workers in production, I’d love to hear about what you’re using them for!