A worker is an object created using a constructor (e.g.
Given that there is a way to use other threads for background processing, why doesn't this happen all the time? Well there's a number of reasons; not the least of which is the ceremony involved in interacting with Web Workers. Consider the following example that illustrates moving a calculation into a worker:
This is not simple. It's hard to understand what's happening. Also, this approach only supports a single method call. I'd much rather write something that looked more like this:
There's a way to do this using a library made by Google called comlink. This post will demonstrate how we can use this. We'll use TypeScript and webpack. We'll also examine how to integrate this approach into a React app.
Let's make ourselves a TypeScript web app. We're going to use
create-react-app for this:
takeALongTimeToDoSomething.ts file alongside
index.tsx add this code:
When our application runs we see this behaviour:
The app starts and logs
Do something and
Start our long running job... to the console. It then blocks the UI until the
takeALongTimeToDoSomething function has completed running. During this time the screen is empty and unresponsive. This is a poor user experience.
To start using comlink we're going to need to eject our
create-react-app application. The way
create-react-app works is by giving you a setup that handles a high percentage of the needs for a typical web app. When you encounter an unsupported use case, you can run the
yarn eject command to get direct access to the configuration of your setup.
Web Workers are not that commonly used in day to day development at present. Consequently there isn't yet a "plug'n'play" solution for workers supported by
create-react-app. There's a number of potential ways to support this use case and you can track the various discussions happening against
create-react-app that covers this. For now, let's eject with:
Then let's install the packages we're going to be using:
worker-plugin- this webpack plugin automatically compiles modules loaded in Web Workers
comlink- this library provides the RPC-like experience that we want from our workers
We now need to tweak our
webpack.config.js to use the
Do note that there's a number of
plugins statements in the
webpack.config.js. You want the top level one; look out for the
new HtmlWebpackPlugin statement and place your
new WorkerPlugin(), before that.
Now we're ready to take our long running process and move it into a worker. Inside the
src folder, create a new folder called
my-first-worker. Our worker is going to live in here. Into this folder we're going to add a
This file exists to tell TypeScript that this is a Web Worker. Do note the
"lib": [ "webworker" usage which does exactly that.
tsconfig.json file, let's create an
index.ts file. This will be our worker:
There's a number of things happening in our small worker file. Let's go through this statement by statement:
Here we're importing the
expose method from comlink. Comlink’s goal is to make exposed values from one thread available in the other. The
expose method can be viewed as the comlink equivalent of
export. It is used to export the RPC style signature of our worker. We'll see it's use later.
Here we're going to import our
takeALongTimeToDoSomething function that we wrote previously, so we can use it in our worker.
Here we're creating the public facing API that we're going to expose.
We're going to want our worker to be strongly typed. This line creates a type called
MyFirstWorker which is derived from our
exports object literal.
Finally we expose the
exports using comlink. We're done; that's our worker finished. Now let's consume it. Let's change our
index.tsx file to use it. Replace our import of
With an import of
wrap from comlink that creates a local
takeALongTimeToDoSomething function that wraps interacting with our worker:
Now we're ready to demo our application using our function offloaded into a Web Worker. It now behaves like this:
There's a number of exciting things to note here:
- The application is now non-blocking. Our long running function is now not preventing the UI from updating
- The functionality is lazily loaded via a
my-first-worker.chunk.worker.jsthat has been created by the
The example we've showed so far demostrates how you could use Web Workers and why you might want to. However, it's a far cry from a real world use case. Let's take the next step and plug our Web Worker usage into our React application. What would that look like? Let's find out.
index.tsx back to it's initial state. Then we'll make a simple adder function that takes some values and returns their total. To our
takeALongTimeToDoSomething.ts module let's add:
Let's start using our long running calculator in a React component. We'll update our
App.tsx to use this function and create a simple adder component:
When you try it out you'll notice that entering a single digit locks the UI for 5 seconds whilst it adds the numbers. From the moment the cursor stops blinking to the moment the screen updates the UI is non-responsive:
So far, so classic. Let's Web Workerify this!
We'll update our
my-first-worker/index.ts to import this new function:
App.tsx file let's create an
makeWorkerApiAndCleanup functions make up the basis of a shareable worker hooks approach. It would take very little work to paramaterise them so this could be used elsewhere. That's outside the scope of this post but would be extremely straightforward to accomplish.
Time to test! We'll change our
App.tsx to use the new
Now our calculation takes place off the main thread and the UI is no longer blocked!