So you want to know about Web Workers?

wardellbagby

Wardell Bagby

Posted on January 21, 2022

So you want to know about Web Workers?

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 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 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.

  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 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.

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.

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 for bundling and Comlink for communication.

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

const myWorker = new Worker(new URL("./path/to/my/file.js"), "my-worker-name");
Enter fullscreen mode Exit fullscreen mode

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:

// 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 });
Enter fullscreen mode Exit fullscreen mode
// 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
  }
Enter fullscreen mode Exit fullscreen mode

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

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!

💖 💪 🙅 🚩
wardellbagby
Wardell Bagby

Posted on January 21, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

#FreeJavaScript
javascript #FreeJavaScript

November 28, 2024

GOTTA GO FAST!
javascript GOTTA GO FAST!

November 28, 2024