Loading Web Workers with Webpack

Web workers, which we already took a look at in a previous post, are very useful for parallelizing long-running CPU-bound work client-side so it doesn't freeze up a website while it's running. If you're using Webpack, you have the option of compiling your worker script as a separate bundle using the worker loader.

The worker loader allows you to import modules in your worker scripts just like you would in the rest of your JavaScript app. The loader also handles creating the Worker object, and supports either downloading the worker script asynchronously when it's needed or inlining it in your script as a blob.

Using the worker loader

First, you'll need to install the worker loader package from NPM:

npm install worker-loader

Then, to load a web worker into your app using the worker loader, just require your worker script like this:

const worker = require('worker!./require/path/to/worker.js');

The worker loader returns a Worker instance, which we looked at in Parallelization in JavaScript with Web Workers. You can call postMessage, terminate, and so on, on the returned worker object:

const worker = require('worker!./require/path/to/worker.js');

worker.onmessage = (message) => {
  console.log(message.data);
};

// start the worker
worker.postMessage({ myProp: true });

By default, the worker loader will output your worker as a separate bundle that gets loaded asynchronously relative to your webpack public path. If you want to inline it in one of your main bundles instead, pass the "inline" parameter to the loader like this:

const worker = require('worker?inline!./require/path/to/worker.js');

If you inline your worker, you can avoid the overhead of making a separate request to download the worker script. On the other hand, doing so could significantly increase the size of the bundle it's inlined in. Which approach you choose will depend on your optimization needs.

The worker loader supports lots of different options for bundling and loading workers, so be sure to go check out the project's readme on github.

How it works

The worker loader effectively treats your worker script as a separate Webpack entry point. When you pass the worker script to the worker loader, it and all of the modules it imports will be compiled and bundled into a separate worker "bundle".

For example - here's a web worker from my Buddhabrot fractal project on github:

import BuddhabrotGenerator from './math/BuddhabrotGenerator';

onmessage = (m) => {
  const sequenceEscapeThreshold = m.data.sequenceEscapeThreshold,
    sequenceBound = m.data.sequenceBound;

  const fractalGenerator = BuddhabrotGenerator({
    sequenceEscapeThreshold,
    sequenceBound
  });

  while (true) postMessage(fractalGenerator.next());
};

When this script is loaded with the worker loader, it will output a separate bundle that contains all of the code in the BuddhabrotGenerator module (and the code for everything it imports). Then, wherever you load the worker using the worker loader, it will construct a Worker object using either a URL to the worker bundle on the server or an inlined blob containing all the worker's code.

An important note on code size

Remember, it's important to think of your worker as a separate Webpack entry point. If you import a module into both your main JavaScript bundle and your worker script, all the code in the module and any modules it depends on will be duplicated in the worker script, potentially greatly increasing the size of your website's code. You'll need to keep this in mind when you're componentizing your worker's dependencies.