Web Workers provide a powerful mechanism for developing complex frontend web applications. They allow a web application to offload long running processes to another thread, allowing for the user to continue interacting with the UI in a smooth, uninterrupted fashion.
Simply launching a worker is easy and well documented here. The roadblock I encountered was using an external image processing library within the worker. The problem is you have to load the entire web worker from a single JavaScript file and can't just import
another package or use a <script>
tag to include a dependency since the worker doesn't have access to the window
object. The solution I came up with was to create a mono repo with one package for my React project and another web worker package. I managed the monorepo using Yarn Workspaces. I've broken this down into four steps:
- Setting up Yarn Workspaces
- Creating the web worker
- Configuring Babel and Webpack to build the worker package
- Launching and communicating with the worker
Let's get started!
Step 1: Setting up Yarn Workspaces
The first step will be to create the directory structure for our project and create a basic React App using create-react-app
:
$ mkdir web-workers-example
$ cd web-workers-example
$ mkdir worker
$ npx create-react-app example-app
Now we're ready to configure workspaces as documented here. First, let's create a top-level package.json
file to manage the workspace:
{
"private": true,
"workspaces": ["worker", "example-app"]
}
Now here the docs will say to add dependencies to the React App to pull in the worker package. We can't do that though because the worker code will be merged into the React code. The solution? Place the worker code directly in the public
directory so it will be accessible to the worker.
Now the only part left is to add some helper scripts for building and developing our application. Add concurrently
to the workspace root so we can dynamically build both packages:
$ yarn add concurrently -W
Now we can add the build scripts. Add the following scripts section to the top-level package.json
file:
"scripts": {
"build": "yarn workspace worker build && yarn workspace example-app build",
"example-app": "yarn workspace example-app start",
"worker": "yarn workspace worker start",
"start": "concurrently --kill-others-on-fail \"yarn worker\" \"yarn example-app\""
},
Now we're ready to setup the worker.
Step 2: Creating the Web Worker
The first thing we need to do is configure the worker's package.json
file. Start by initializing an empty package in the worker
directory:
$ cd worker
$ yarn init -y
Next, let's create a very simple worker. To illustrate using an external library, we will use the a math expression evaluator package that will do some basic calculations for the React App. Add the package:
$ yarn add math-expression-evaluator
The worker code will live in index.js
which is the entry point specified in package.json
. Our worker will listen for a math problem received in a message from the React App, evaulate it, and send back the results:
import mexp from "math-expression-evaluator"
// Whenever the worker receives a message:
self.onmessage = event => {
try {
// Evaulate the math problem.
const result = mexp.eval(event.data)
// Return the result of the calculation.
self.postMessage(result)
} catch {
// If there was an error, let the user know.
self.postMessage("Please enter a valid math problem")
}
}
Now we have a service worker! The problem is the external library, in this case math-expression-evaluator, is in a separate file from our worker, so copying index.js
to the React App's public
directory won't work. Instead, we can leverage Babel and webpack to transpile the code and combine it into one file.
Step 3: Configuring Babel and Webpack to build the worker package
Before doing anything else, let's add the Babel and webpack packages as development dependencies to our worker package:
$ yarn add -D @babel/core @babel/cli @babel/preset-env babel-loader webpack webpack-cli
Setting up Babel is simple, so we'll do that first. Create a basic Babel configuration file .babelrc
in the project root:
{
"presets": ["@babel/preset-env"]
}
Now for webpack. Create a webpack configuration file called webpack.config.js
in the project root that instructs Webpack to use Babel to create one Javascript file. It will combine index.js
and its dependencies to create a worker.js
file in the React App's public
directory:
const path = require("path")
module.exports = {
entry: "./index.js",
output: {
filename: "worker.js",
path: path.join(__dirname, "../example-app/public/"),
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: ["babel-loader"],
},
],
},
}
The last step is to add scripts to package.json
to build our project:
"scripts": {
"start": "webpack --mode development --watch",
"build": "webpack --mode production"
},
Fabulous! Now running yarn build
will automatically create a worker.js
file and place it in the public directory of our React App.
Step 4: Using the worker in the React App
Before wiring up the worker, let's create the default component. It will be a simple form where a user can enter a math problem and then will see the result caculated by our math problem solving worker. Replace the code in App.js
with the following:
import React, { useState } from "react"
export default function App() {
const [mathProblem, setMathProblem] = useState("")
const [calculationResults, setCalculationResults] = useState("")
const calculate = async () => {
// Calculation happens here
}
return (
<div>
<section>
<h3>Enter a math problem</h3>
<input
value={mathProblem}
onChange={e => setMathProblem(e.target.value)}
/>
<br />
<button onClick={calculate}>Calculate</button>
</section>
<section>
<h3>Result</h3>
<p>{calculationResults}</p>
</section>
</div>
)
}
To increase visual appeal, we'll add New.css, a lightweight CSS framework that styles plain HTML elements without the need for classes. Add the following two lines in the <head>
of public/index.html
:
<link rel="stylesheet" href="https://fonts.xz.style/serve/inter.css" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@exampledev/new.css@1/new.min.css"
/>
Now you should have a page that looks like this:
Now let's get the worker wired up! Initialize the worker using the Worker call:
// Initialize the worker
const worker = new Worker(process.env.PUBLIC_URL + "/worker.js")
I like to wrap the worker communication inside of a promise to encapsulate the worker actions. It keeps all the worker code in the same place and clarifies what's going on. We'll encapsulate our worker call in a doTheMath function outside of the component. doTheMath will send the math problem the user enters and receive the result.
const doTheMath = mathProblem => {
// Create a promise to handle the worker message
return new Promise(resolve => {
// Return the result when it comes back from the worker.
worker.onmessage = event => {
resolve(event.data)
}
// Send the math problem
worker.postMessage(mathProblem)
})
}
The last step is to update the calculate
function to call the doTheMath
function.
const calculate = async () => {
const result = await doTheMath(mathProblem)
setCalculationResults(result)
}
The final App.js
file should look like this:
import React, { useState } from "react"
// Initialize the worker
const worker = new Worker(process.env.PUBLIC_URL + "/worker.js")
const doTheMath = mathProblem => {
// Create a promise to handle the worker message
return new Promise(resolve => {
worker.onmessage = event => {
resolve(event.data)
}
worker.postMessage(mathProblem)
})
}
export default function App() {
const [mathProblem, setMathProblem] = useState("")
const [calculationResults, setCalculationResults] = useState("")
const calculate = async () => {
const result = await doTheMath(mathProblem)
setCalculationResults(result)
}
return (
<div>
<section>
<h3>Enter a math problem</h3>
<input
value={mathProblem}
onChange={e => setMathProblem(e.target.value)}
/>
<br />
<button onClick={calculate}>Calculate</button>
</section>
<section>
<h3>Result</h3>
<p>{calculationResults}</p>
</section>
</div>
)
}
Let's try it out! From the workspace root, run yarn start
. This will build the worker, place it in the public
folder in the React App and start the React App. Here's an example of the app in action:

Our web worker did its job! Give it a shot with more complex problems and see how it does. This is a trivial example but now you're prepared to explore the world of web workers.
Thanks for reading!
Helpful Links