Getting Started with JavaScript Web Workers and Off-Main-Thread Tasks
Tags: javascript, beginners, webdev, tutorial
Published: 2019-12-26 15:24:42

---
title: "Getting Started with JavaScript Web Workers and Off-Main-Thread Tasks"
published: true
description: "Learn how you can use Web Workers to run JavaScript code off the main thread and keep your animations running smoothly"
tags: javascript, beginners, webdev, tutorial
---

JavaScript in the browser is single-threaded by design, meaning that all of our JavaScript code will share the same call stack. At first glance, this seems a bit implausible; we perform concurrent operations all the time using [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). However, this concurrency (along with `setTimeout`, `setInterval` and others) is achieved using the [event loop](https://www.youtube.com/watch?v=cCOL7MC4Pl0). 

Usually, this is more than enough, especially for apps that mostly fetch data and display it, or accept input and persist it using HTTP and a server. However, as client-side apps continue to become more complex and "app-like" we tend to run an increasing amount of JavaScript in the browser, which places stress on our one single thread (or the "main thread"). Fortunately, we have [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) to help us relieve the main thread by running JavaScript code in background threads!

## What is a Web Worker?

Per MDN, [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. They are not to be confused with Service Workers, which are concerned with proxying your application's network requests. The value of Web Workers is that they enable **parallelism**, giving your application the ability to run multiple JavaScript execution contexts at the same time. 

There are a couple of important limitations to consider when using Web Workers:
1. Web Workers execute in a completely separate JavaScript environment and don't share memory with your main thread, instead communicating with messages
2. Workers have a different global scope than the main JS thread: there is no `window` object, and thus there is no DOM, no `localStorage` and so on
3. The actual JS code for your worker has to live in a separate file (more on this later)

Although they are used somewhat infrequently, Web Workers have been around for a long time and are supported in every major browser, even going back to IE 10 ([source](https://caniuse.com/#search=web%20workers))

![Web Workers API browser support chart](https://thepracticaldev.s3.amazonaws.com/i/rg0d5t1r0z6ido0yyk82.png)

> ### Quick note on concurrency vs. parallelism
> Although concurrency and parallelism seem at first to be two terms referring to the same concept, they are different. In short, **concurrency** is making progress on multiple tasks without doing them in order; think of your application invoking `fetch` multiple times and performing some task after each Promise resolves, but not necessarily in order and without blocking the rest of your code. **Parallelism**, however, is doing multiple things *at the same time*, on multiple CPUs or multiple CPU cores. For more on the topic, check out [this awesome StackOverflow post](https://stackoverflow.com/questions/1050222/what-is-the-difference-between-concurrency-and-parallelism) that has several different explanations!

## Basic example

Alright, enough exposition, let's look at some code! To create a new `Worker` instance, you must use the constructor, like so:

```js
// main.js
const worker = new Worker('path/to/worker.js');
```

As mentioned above, this path does have to actually point to a separate JavaScript file from your main bundle. As such, you may have to configure your bundler or build chain to handle Web Workers. If you are using [Parcel](https://parceljs.org/), Web Workers are handled out of the box! Therefore, we'll use Parcel for the rest of this post. Using Parcel, you can construct a Worker instance by passing a *relative path* to the actual source code for your worker instead, like so:

```js
// main.js
const worker = new Worker('./worker.js');
```

> NOTE: If you are using Parcel within CodeSandbox, this particular feature isn't currently supported. Instead, you can clone a Parcel boilerplate like [this one](https://github.com/thecodercoder/boilerplate-parcel) or make your own, and experiment locally.

This is great, because now we can use NPM modules and fancy ESNext features in our Worker code, and Parcel will handle the task of spitting out separate bundles for us! 🎉

Except, `worker.js` doesn't exist yet... let's create it. Here's the minimal boilerplate for our Web Worker:

```js
// worker.js
function handleMessage(event) {
  self.postMessage(`Hello, ${event.data}!`);
}

self.addEventListener('message', handleMessage);
```

Notice that we use `self` here rather than `window`. Now, let's go back to our main script and test out our Worker by posting a message to it and handling the response:

```js
// main.js
const worker = new Worker('./worker.js');

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

worker.addEventListener('message', handleMessage);

worker.postMessage('Mehdi');
// Hello, Mehdi!
```

That should do the trick! This is the minimal setup for working with a Web Worker. A "hello world" app is not exactly CPU-intensive however... let's look at a slightly more tangible example of when Web Workers can be useful.

## Bouncy ball example

For the sake of illustrating the usefulness of Web Workers, let's use a recursive [Fibonacci sequence](https://en.wikipedia.org/wiki/Fibonacci_number) calculator that performs its job super inefficiently, something like this:

```js
// fib.js
function fib(position) {
  if (position === 0) return 0;
  if (position === 1) return 1;
  return fib(position - 1) + fib(position - 2);
}

export default fib;
```
 
In the middle of our calculator, we want to have a bouncy ball, like so:

![Bouncy ball beside Fibonacci calculator form](https://thepracticaldev.s3.amazonaws.com/i/v3p7ahrfq9b29z90gkjy.gif)

The bounce animation is happening in a `requestAnimationFrame` loop, meaning that the browser will try to paint the ball once every ~16ms. If our main-thread JavaScript takes any longer than that to execute, we will experience dropped frames and visual jank. In a real-world application full of interactions and animation, this can be very noticeable! Let's try to calculate the Fibonacci number at position `40` and see what happens:

![Bouncy ball freezing while calculating](https://thepracticaldev.s3.amazonaws.com/i/j6hxvm1flcd8i0k53i0u.gif)

Our animation freezes for at least 1.2 seconds while our code is running! It's no wonder why, as the recursive `fib` function is invoked a total of 331160281 times without the call stack being cleared. It's also important to mention that this depends entirely on the user's CPU. This test was performed on a 2017 MacBook Pro. With CPU throttling set to 6x, the time spikes to over 12 seconds.

Let's take care of it with a Web Worker. However, instead of juggling `postMessage` calls and event listeners in our application code, let's implement a nicer Promise-based interface around our Web Worker.

First, let's create our worker, which we will call `fib.worker.js`:

```js
// fib.worker.js
import fib from './fib';

function handleMessage(event) {
  const result = fib(event);
  self.postMessage(result);
};

self.addEventListener('message', handleMessage);
```

This is just like our previous Worker example, except for the addition of a call to our `fib` function. Now, let's create an `asyncFib` function that will eventually accept a position parameter and return a Promise that will resolve to the Fibonacci number at that position. 

```js
// asyncFib.js
function asyncFib(pos) {
  // We want a function that returns a Promise that resolves to the answer
  return new Promise((resolve, reject) => {
    // Instantiate the worker
    const worker = new Worker('./fib.worker.js');

    // ... do the work and eventually resolve
  })
}

export default asyncFib;
```

We know that we will need to handle messages from our worker to get the return value of our `fib` function, so let's create a `message` event handler that captures the message and resolves our Promise with the data that it contains. We will also invoke `worker.terminate()` inside of our handler, which will destroy the Worker instance to prevent memory leaks:

```js
// asyncFib.js
function asyncFib(pos) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./fib.worker.js');

    // Create our message event handler
    function handleMessage(e) {
      worker.terminate();
      resolve(e.data);
    }

    // Mount message event handler
    worker.addEventListener('message', handleMessage);
  })
}
```

Let's also handle the `error` event. In the case that the Worker encounters an error, we want to reject our Promise with the error event. Because this is another exit scenario for our task, we also want to invoke `worker.terminate()` here:

```js
// asyncFib.js
function asyncFib(pos) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./fib.worker.js');

    function handleMessage(e) {
      worker.terminate();
      resolve(e.data);
    }

    // Create our error event handler
    function handleError(err) {
      worker.terminate();
      reject(err);
    }

    worker.addEventListener('message', handleMessage);
    // Mount our error event listener
    worker.addEventListener('error', handleError);
  })
}
```

Finally, let's call `postMessage` with the `pos` parameter's value to kick everything off!

```js
// asyncFib.js
function asyncFib(pos) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./fib.worker.js');

    function handleMessage(e) {
      worker.terminate();
      resolve(e.data);
    }

    function handleError(err) {
      worker.terminate();
      reject(err);
    }

    worker.addEventListener('message', handleMessage);
    worker.addEventListener('error', handleError);

    // Post the message to the worker
    worker.postMessage(pos);
  })
}
```

And that should do it. One last thing left to do: check to make sure it works. Let's see what our app looks like when calculating the Fibonacci number at position `40` with our new `asyncFib` function:

![Bouncy ball continuing to bounce while calculating](https://thepracticaldev.s3.amazonaws.com/i/cmjht5fo34nqiqhxdvf5.gif)

Much better! We've managed to unblock our main thread and keep our ball bouncing, while still creating a nice interface for working with our `asyncFib` function. 

If you are curious, [play around with the example app](https://web-worker-example.netlify.com/) or [check out the code on GitHub](https://github.com/mvasigh/intro-web-workers).

## Wrapping up

The Web Worker API is a powerful and underutilized tool that could be a big part of front-end development moving forward. Many lower-end mobile devices that make up a huge percentage of web users today have slower CPUs but multiple cores that would benefit from an off-main-thread architecture. I like to share content and write/speak about Web Workers, so [follow me on Twitter](https://twitter.com/mehdi_vasigh) if you're interested.

Here are also some other helpful resources to get your creative juices flowing:

* ["When should you be using Web Workers?" - Surma](https://dassur.ma/things/when-workers/)
* [Comlink](https://github.com/GoogleChromeLabs/comlink)
* [Workerize](https://github.com/developit/workerize)
* [worker-loader (Webpack loader)](https://github.com/webpack-contrib/worker-loader)

Thanks for reading!