So you want to know about Web Workers?
Tags: javascript, webdev
Published: 2022-01-21 23:55:31

Hey! Wardell here! So you've got yourself a website and you're interested in potentially off-loading some of your heavy-hitting computations to another thread?

Sounds like you want a Web Worker!

---
## Hold up, what even _is_ a Web Worker?

> [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) are a simple means for web content to run scripts in background threads.

Before I tell you _how_ to use a Web Worker, let's first go over things to consider before deciding to use a Web Worker.

1. You've [profiled](https://developer.chrome.com/docs/devtools/evaluate-performance/) your website and discovered what you're considering putting in a Web Worker is actually slow.
  - Don't bother with a Web Worker if you don't actually need it. Adding an extra thread to communicate with will complicate your code, and if you aren't getting noticeable performance gains from it, it's an unnecessary complication.

2. The work you want to offload _can_ be done asynchronously.
  - This is difficult to define, but [if you know, you know](https://www.youtube.com/watch?v=L7-0ugujS2U).
3. You don't need `window`, `document`, or anything else DOM related.
  - Web Workers don't have direct access to the DOM of your site. There are ways around this but it's generally just a good idea _not_ to do it in the first place.

---

## What's something you've used a Web Worker for?

See, I just love your questions!

My app [Lyricistant](https://lyricistant.app) currently has two web workers, but we're going to talk about the easier to understand one: [a Web Worker that generates rhymes on demand, totally offline](https://github.com/wardellbagby/lyricistant/blob/7e047c48a112076545551ee720bef01c2cef1f5c/packages/rhyme-generator/main/rhyme-generator.ts).

I won't get into the nitty-gritty of how it all works, 'cause I mostly wrote it in a tired stupor at 2am one night, but it needs to load a 6 MiB JSON file, iterate through 135,165 words, and compare the syllables of those 135k words to the syllables of an inputted word.

That is to say, if you type "Time" into Lyricistant (make sure to enable the Offline Rhymes in Preferences first!), my web worker will:

1. Find the pronunciation of "time" (T AY1 M)
2. Iterate over _every single one_ of the 135k words it has pronunciations for.
3. Compare the syllables of "time" to the syllables of the word its currently looking at.
4. Calculate a score based off the comparison in step 3.

It's also worth noting that this all happens on almost _every_ keypress (its debouncing, of course; I'm not a monster), so not only are we finding rhymes for "time", but also "tim" and "ti" if you type slow enough.

This is a **very** naive way of generating rhymes, and also extremely slow. My excuse is I'm not a linguist! I'm barely a programmer! 😂

Anyway, you can imagine how slow all of that code can be, so a Web Worker was the perfect choice to use! Although, fun fact: my initial implementation had this all run in the browser's main thread, and 60% of the time, it would finish in under 60 milliseconds, meaning Lyricistant could _mostly_ hit 60fps on my 2015 MacBook Pro using Chrome. Browsers are pretty fast!


___
## Alright, enough talking. I wanna make my own Web Worker!

Fair enough; this blog post was starting to look like one of those [online recipe intros](https://www.reddit.com/r/mildlyinfuriating/comments/bcuvs3/literally_every_online_recipe_now_days/).

### Creating your Web Worker

There are quite a few ways to make a Web Worker, but probably the easiest for most people will be using [Webpack](https://webpack.js.org/) for bundling and [Comlink](https://github.com/GoogleChromeLabs/comlink) for communication.

Assuming you're already using Webpack, making your Web Worker is super easy:

```javascript
const myWorker = new Worker(new URL("./path/to/my/file.js"), "my-worker-name");
```

The bit you give to `URL` should match exactly what it'd look like in an `import` or `require` statement. This works with any path mappings or resolve aliases you might have set up as well.

I.e, if you'd normally import the file like `import '@my-app/my-file.ts'`, then you'd do `new URL('@my-app/my-file.ts')`.

If you're _not_ using Webpack, you should probably consult your own module bundler's docs. If you're not using any bundler, omit the `new URL` and instead do `new Worker("./path/to/your/output/file.js");`

### Communicating with your Web Worker

This is where Comlink comes into play!

Web Workers, at their core, communicate via posting messages back and forth. Your main thread code communicates with your Worker with `worker.postMessage`, your Worker listens to those messages with `self.onmessage`, your Worker responds with `self.postMessage`, and your main thread listens to those responses with `window.onmessage`.

That's not very easy to follow, is it?

Comlink removes all that hassle and instead gives you a much nicer `Promise`-based API.

Let's imagine you've got a Web Worker that simply multiples two numbers. With Comlink, you can set that up like this:

```typescript
// Worker code math-worker.ts
import { expose } from "comlink";

const multiply = (multiplicand: number, multiplier: number): number =>  {
  return multiplicand * multiplier
}

// Every function we "expose" this way will be available in the main thread. Functions that aren't exposed won't be available.
expose({ multiply });
```

```typescript
// Main thread code
import { wrap } from "comlink";

const mathWorker = new Worker(new URL("./math-worker.ts"), "math-worker");

const math = wrap(mathWorker);

// Wrapping mathWorker gives us access to the exposed functions, but now they return Promises!
math.multiply(2, 2)
  .then((product) => {
    console.log(product) // 4
  }
```

---

## Know anything I should be on the lookout for? Or just general advice?

As I mentioned earlier, your Worker doesn't have access to the DOM or `window`. That makes sense; you don't actually have a DOM in a Web Worker because you don't have any UI. Outside of that, you can do almost anything you want, including spawning _more_ Workers!

You also can't use `this` at the root level of your Worker code; use `self` instead. `this` still works fine in functions and classes.

**Verify that your code is actually running in a Web Worker!** This bit me a few times, where I had messed up the setup and had inadvertently ran my worker in the main thread. Super easy to do if you import the file your Worker is supposed to run directly as an `import`. The easiest way to verify your Web Worker is running is by opening up Dev Tools and going to the "Sources" tab. You should see something like this:


![Image of Chrome Dev Tools](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pm9ey63obj2wzbatc219.png)

In that image, "top" refers to the main thread, and "platform" and "rhyme-generator" are Web Workers.

If you only see "top", your Web Worker isn't running. This is either because you haven't started it yet (which you do by sending some data to it) or because you've misconfigured it.

Also, remember that concurrency is difficult! Try and keep your Workers as simple and stateless as possible. This'll make your life much easier overall.

One last tip: much like regular threads, there's diminishing returns to having too many Web Workers. A tip that I've heard is the maximum number of Web Workers you should spawn is `window.navigator.hardwareConcurrency - 1`. We subtract one to save a core for the main thread. 

___

## Alright, I think I've got it now! Thanks!

Of course, no problem! Have fun and don't work your workers too hard!