Loading web workers using Webpack 5
Tags: javascript, webdev, development, webpack
Published: 2021-09-06 11:59:07

---
title: Loading web workers using Webpack 5
published: true
date: 2021-09-06 11:58:00 UTC
tags: #javascript #webdev #development #webpack
canonical_url: https://mmazzarolo.com/blog/2021-09-03-loading-web-workers-using-webpack-5/
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1s15ac3gn7t9300veg3r.png
---

Just wanted to share a few notes around the currently available options for loading web workers using webpack 5.

## Web Workers overview

Web workers allow you to push work outside of main execution thread of JavaScript, making them convenient for lengthy computations and background work.

Web workers are delivered as scripts that are loaded asynchronously using the [Web Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Worker).

A worker is an object created using a constructor (e.g., [`Worker()`](https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker)) that runs a named JavaScript file.

To create a new worker, all you need to do is call the [`Worker()`](https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker) constructor, specifying the URI of a script to execute:

```js
// Assuming we're in a JavaScript script that runs in your main thread and that
// the worker script is available at yourdomain.com/worker.js, this will take
// care of spawning a new worker:
const myWorker = new Worker("worker.js");
```

Since they’re loaded as separate scripts, web workers can’t be “bundled” within the code that runs in the main thread. This means that if you’re using a module bundler to bundle your code (e.g., Webpack, Rollup) you’ll have to maintain two separate build processes, which can be pretty annoying.

The good news is that, if you’re using webpack, there are a couple of tools you can use to simplify the loading process of web workers.

## Web Workers in webpack 5

Since webpack 5, [web workers are first-class citizens](https://webpack.js.org/guides/web-workers/), and you can use a specific syntax to let webpack automatically handle the creation of two separate bundles.  
To do so, you must use the [`import.meta` object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import.meta) (an object that exposes context-specific metadata) to provide the module URL to the `Worker()` constructor:

```js
const myWorker = new Worker(new URL("./worker.js", import.meta.url));
```

> Note that the `import.meta.url` construct is available only available in ESM. Worker in CommonJS syntax is not supported.

As of today, there’s not much documentation around webpack 5’s web worker supports. It indeed works pretty well for the most common use-cases and it’s a future-proof way to load web worker, but, for now, if you’re looking for a more flexible way to load web workers, you might want to take a look at [`worker-loader`](https://github.com/webpack-contrib/worker-loader).

## Webpack 5 and Worker Loader

[`worker-loader`](https://github.com/webpack-contrib/worker-loader) is the pre-webpack-5 way to load web workers, and its documentation highlights how it’s **not** compatible with webpack 5 (_“Worker Loader is a loader for webpack 4”_).  
Still, in my experience, besides a few quirks, `worker-loader` **can** be used with webpack 5, and it offers [several more customization options](https://github.com/webpack-contrib/worker-loader#options) than webpack 5’s built-in web worker support.

The most important ones are probably the support for inlining web workers as `BLOB` and specifying a custom `publicPath`.

### Inlining web workers

Web workers are restricted by a [same-origin policy](https://en.wikipedia.org/wiki/Same-origin_policy), so if your webpack assets are not being served from the same origin as your application, their download may be blocked by your browser.

This scenario can commonly occur if you are serving the web worker from localhost during local development (e.g., with `webpack-dev-server`):

```js
// If the origin of your application is available at a different origin than
// localhost:8080, you won't be able to load the following web worker:
const myWorker = new Worker(
  new URL("http://localhost:8080/worker.js");
);
```

`worker-loader` solves the local development issue by allowing you to inlining the web worker as a `BLOB` (instead of pointing it to localhost) on development builds by specifying an `inline: "fallback"` option:

```js
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        loader: "worker-loader",
        options: { inline: isDevelopment ? "fallback" : "no-fallback" },
      },
    ],
  },
};
```

### Setting a worker-specific `publicPath`

Another scenario where the same-origin policy might need some accommodations is if you’re hosting your main bundle code on a static CDN.  
In this case, you’re probably going to set the `publicPath` of your webpack `output` to the CDN domain (e.g., `https://my-static-cdn`), so that all the assets will reference it in production. Unfortunately, this pattern doesn’t work well when using web workers because you can’t reference a web worker that is hosted on a CDN (because of the same-origin policy):

```js
// Since the origin of the application (e.g., https://example.com) is different
// from the CDN one, you won't be able to load the following web worker:
const myWorker = new Worker(
  new URL("https://my-static-cdn/worker.js");
);
```

What’s great about `worker-loader`, is that you can solve this issue by setting a worker-specific `publicPath`:

```js
module.exports = {
  output: {
    // Set the publicPath of all assets generated by this webpack build to
    // https://my-static-cdn/.
    publicPath: "https://my-static-cdn/",
  },
  module: {
    rules: [
      {
        loader: "worker-loader",
        // Overrides the publicPath just for the web worker, marking it as
        // available on the same origin used by the app (notice that this is
        // a relative path).
        options: { publicPath: "/workers/" },
      },
    ],
  },
};
```

#### A note on setting worker-loader’s `publicPath` with webpack 5

[Webpack 5 introduced a mechanism to detect the `publicPath` that should be used automatically](https://webpack.js.org/guides/public-path/#automatic-publicpath). Sadly, the new automatic detection seems to be incompatible with `worker-loader`’s `publicPath`… but there are a couple of (hacky) ways you can solve this issue ;)

The first one is to by setting the `publicPath` on the fly.  
Webpack 5 exposes a global variable called ` __webpack_public_path__ ` that allows you to do that.

```js
// Updates the `publicPath` at runtime, overriding whatever was set in the
// webpack's `output` section.
__webpack_public_path__ = "/workers/";

const myWorker = new Worker(
  new URL("/workers/worker.js");
);

// Eventually, restore the `publicPath` to whatever was set in output.
__webpack_public_path__ = "https://my-static-cdn/";
```

> I recommend using webpack’s [`DefinePlugin`](https://webpack.js.org/plugins/define-plugin/) to pass the public path as environment variables instead of hardcoding them in the source code.

The other (even more hacky) option is to apply the following patch to `worker-loader` (using [`patch-package`](https://www.npmjs.com/package/patch-package), for example):

```diff
# worker-loader+3.0.8.patch
# Compatible only with worker-loader 3.0.8.
diff --git a/node_modules/worker-loader/dist/utils.js b/node_modules/worker-loader/dist/utils.js
index 5910165..2f2d16e 100644
--- a/node_modules/worker-loader/dist/utils.js
+++ b/node_modules/worker-loader/dist/utils.js
@@ -63,12 +63,14 @@ function workerGenerator(loaderContext, workerFilename, workerSource, options) {
   const esModule = typeof options.esModule !== "undefined" ? options.esModule : true;
   const fnName = `${workerConstructor}_fn`;

+ const publicPath = options.publicPath ? `"${options.publicPath}"` : ' __webpack_public_path__';
+
   if (options.inline) {
     const InlineWorkerPath = (0, _loaderUtils.stringifyRequest)(loaderContext, `!!${require.resolve("./runtime/inline.js")}`);
     let fallbackWorkerPath;

     if (options.inline === "fallback") {
- fallbackWorkerPath = ` __webpack_public_path__ + ${JSON.stringify(workerFilename)}`;
+ fallbackWorkerPath = `${publicPath} + ${JSON.stringify(workerFilename)}`;
     }

     return `
@@ -77,7 +79,7 @@ ${esModule ? `import worker from ${InlineWorkerPath};` : `var worker = require($
 ${esModule ? "export default" : "module.exports ="} function ${fnName}() {\n return worker(${JSON.stringify(workerSource)}, ${JSON.stringify(workerConstructor)}, ${JSON.stringify(workerOptions)}, ${fallbackWorkerPath});\n}\n`;
   }

- return `${esModule ? "export default" : "module.exports ="} function ${fnName}() {\n return new ${workerConstructor}( __webpack_public_path__ + ${JSON.stringify(workerFilename)}${workerOptions ? `, ${JSON.stringify(workerOptions)}` : ""});\n}\n`;
+ return `${esModule ? "export default" : "module.exports ="} function ${fnName}() {\n return new ${workerConstructor}(${publicPath} + ${JSON.stringify(workerFilename)}${workerOptions ? `, ${JSON.stringify(workerOptions)}` : ""});\n}\n`;
 } // Matches only the last occurrence of sourceMappingURL
```

For more info, check this [GitHub issue](https://github.com/webpack-contrib/worker-loader/issues/281).


> Originally published at [mmazzarolo.com](https://mmazzarolo.com/blog/2021-09-03-loading-web-workers-using-webpack-5/)