Getting Started with Web Workers via Webpack

The Web Workers API provides a mechanism for running background tasks in the browser, safely and with good cross-browser support. They can be used to offload heavy or otherwise blocking tasks from the main execution context, keeping the rest of the application responsive.

In this post we’ll provide a quick recipe for bringing up your first web worker with Webpack in the mix and share some tips to keep your implementation clean.

We’ll be using the Worker interface of the very well-supported Web Workers API.

What is a Worker?

The API provides a Worker constructor that allows us to create objects representing a background script in the browser.

web_workers_block_diagram

 

A Worker is isolated and communication is carried out over a messaging interface, which will be familiar to anyone who has dealt with inter-process communications but perhaps alien to those used to working with browser-style JavaScript.

A Worker is:

  1. Launched based on a separately loaded js script
  2. Isolated in its own execution context
  3. Accessed via a messaging interface
  4. Only accessible from the script that created it
  5. Slow to launch but can be long-lived

Launching a Worker with Webpack

In a classic asset-loading scenario we create an instance of a Worker by statically loading a separate script like so:

var myWorker = new Worker('js/worker.js');

Luckily we can easily use this in our ES6 / Webpack stack thanks to the worker-loader module.

import MyWorker from "worker!./file.js";
const worker = new MyWorker();

The loader provides a new constructor for each Worker script we load, and if you are transpiling with Babel as we do, your Worker script can enjoy ES6 goodness too.

The Simplest Possible Worker

The Web Worker interface is actually very simple and incredibly flexible. Once you understand the messaging mechanism, it’s straightforward to implement a messaging pattern of your choosing to support the task at hand, across one or more Web Workers.

Let’s explore basic messaging with a simple example.

First we make the distinction between the Host Script and the Worker Script. In a toy example, we would like the following interactions:

  • The Host creates the Worker.
  • The Host sends a “get started please” message to the Worker.
  • On receiving a “get started please” message, the Worker begins the background task.
  • When the task is done, the Worker sends a message back to the Host containing some new data.
  • On receiving a message, the Host prints the data to the console and terminates the Worker.

It’s fairly simple but representative of the messaging we’d need for a one-shot background task.

The Host Script is responsible for launching the Worker. The object instance created then provides the messaging interface to our worker as shown below:

import MyWorker from "worker!./file.js"
	
const launchWorker = () => {
      const worker = new MyWorker();

      myWorker.onmessage = (e.data) => {
            const message = e.data;
            console.log("Host received: ", message);
            If (message.type === 'done!') {
	          myWorker.terminate();
            }
      }
}

const myWorker = launchWorker();
console.log("Host: posting 'start'");
myWorker.postMessage({ type: "start" });

Here we define a function that creates the Worker and registers a ‘onmessage’ handler. This is not the only way to bind the handler but it is the most convenient. In that handler, we retrieve our message from the `data` property on the incoming event object, print it and then terminate the Worker. To actually start our background task we call the function and then post a “reply please” message to the Worker. The Worker Script is responsible for setting up appropriate the message handling, passing on the Worker and carrying out that actual processing task that we want to handle in the background. Worker scripts are completely isolated from the Host script’s execution context and operate within the context of their own with access to a WorkerGlobalScope. As we are using a dedicated Worker here, we actually have access to the DedicatedWorkerGlobalScope carrying the `onmessage` handler that we will use in the example below:

import iterateOnPI from "mytasks/iterateOnPI";

onmessage = (e) => {
      const message = e.data;
      
      console.log("Worker: received '"+message+"'"); 

      if (message.type === "start") {
            let numIter = 100;
            let pi = iterateOnPI(0);

            for (const i = 0; i < numIter; i++) {
	          pi = iterateOnPI(pi);
	          if (i % 10 === 0) {
		        postMessage({
                              type: "progress",
                              value: i / numIter;
                             });
	          }
            }

            postMessage({type: "done!", value: pi});
       }
}

In the Worker script, thanks to our Webpack worker-loader plus Babel, we have ES6 imports available as per the rest of our build, so we can go ahead and load any scripts we like here to carry out the background work.

In this example, we listen on the global `onmessage` event for our specific “start” message; when that is received we kick off our blocking task to compute PI over 100 iterations, and when the resulting promise is fulfilled we post a message back to the Host with our data. As we are iterating, it’s easy to update the Host on progress, so we do that too, posting a “progress” message every 10 iterations.

When running this code, our Console Output would look something like:

Host: posting 'get started please'
Worker: received { type: 'start' }
Host: received { type: 'progress', value: 0.1 }
Host: received { type: 'progress', value: 0.2 }
…
Host: received { type: 'progress', value: 0.9 }
Host: received { type: 'progress', value: 1.0 }
Worker: compute finished
Host: received { type: 'done!', value: 3.141592653589793 }

Wrap-Up

Web Workers are a very convenient way to offload heavier tasks, keeping your browser’s main thread nice and responsive, and that’s often the primary driver for implementing one. However, Web Workers provide a way to essentially multi-thread or componentize an application that is more amenable to computationally heavy or complex tasks.

Although implementing high-performance computing in Chrome for large data sets may not be that sensible many cases, being able to build web applications that can reasonably perform computation close to the presentation / visualization layer gives developers an additional edge on creating highly interactive experiences in the browser or in desktop applications built with web stacks.

Stay tuned for more posts on Web Workers.