A Guide to using Web Workers in React
Web Workers are a way of running web scripts in background threads without blocking the main thread. JavaScript is a single-threaded environment, which means that multiple scripts cannot be run at the same time. A single thread means that every line of JS code is executed one at a time.
For example, let’s look at a website that needs to do the following, handle UI interactions, interact with and process API responses, and manipulate the DOM. These are fairly common in a modern website/web app. Unfortunately all of the above cannot be simultaneous due to limitations in browsers’ JavaScript runtime. Script execution always happens within a single thread.
Therefore, if any of the activity above takes too much time to happen, it can cause blockage to the main thread and render the whole app unusable.
So how can this problem of thread blocking be solved?
By introducing multi-threading. Multi-threading allows the browser to run multiple scripts without causing an interruption to the main thereby resulting in a performant and responsive UI.
This is where Web Workers come in. Web Workers allow JavaScript code to run in a separate and background thread, entirely independent of the browser thread and its usual activities.
Since these workers run on a separate thread than the main execution thread, you can utilize web workers to run process intensive tasks from the browser without creating blocking instances.
Completed Code
The complete codebase for the React app above can be viewed on GitHub and also CodeSandbox.
You can test this out on the live app at https://build-jrrpxladjd.now.sh/.
Web Workers run in an isolated thread. As a result, the code that they execute needs to be contained in a separate file. Because, the worker script runs in another script. To get started with Web Workers, you need to create a new Worker
object in your main page.
// main.js file
var worker = new Worker("workerfile.js");
The Worker()
constructor call creates a worker and returns a Worker
object representing that worker, which is used to communicate with the worker. How do we communicate with a worker? By calling the postMessage()
method.
// main.js file
worker.postMessage("Hello World");
In the workerfile.js
file, we’d have something like this:
// workerfile.js
self.addEventListener(
"message",
function(e) {
self.postMessage(e.data);
},
false
);
The event listener listens for any message event and then acts on the message by running the code inside the function in this case, sending a message back to the main thread with a postMessage() function.
Note: We always send a message back to the main thread.
Finally, we will also need a message event listener in the main file to receive the data and act upon it. Something like the code block below.
// main.js file
var worker = new Worker('workerfile.js');
worker.addEventListener('message', function(e) {
console.log('Message from Worker: ' + e.data);
}
worker.postMessage('Hello World');
Web Workers in React
Let’s explore how Web Workers can be used in a React app. In this example, we’ll see how a CPU-intensive action can cause blockage to the UI and then fix the issue with a Web Worker script.
We’ll write a for loop that’s a bit intensive and causes a blockage to the main thread and see how it behaves when called directly and through a Web Worker.
To get started, we’ll use the create-react-app package to bootstrap a new React app or you can follow using CodeSandbox.
You can create a new React app with the command below:
npx create-react-app react-worker
Once the app has been created and installed, create a file named Home.js
in the src
folder.
Before we continue with setting up the project, we’ll need to install a React package called react-countdown-clock. It basically renders a countdown timer and it has no effect on what we’re building, however it’s going to be used as an example for what happens when the main thread is being blocked. You can install with the command below.
npm i react-countdown-clock
In the newly created Home.js
file, edit it with the following code block:
In the code block above, we start by importing React
, Component
and the ReactCountdownClock
package we installed earlier.
In the render
function, we then create the necessary UI for the app. We are also using the ReactCountdownClock
package by rendering it.
The important bit in the code above is the fetchUsers
function. The function contains a for loop that runs 10,000,000 times. It’s a very impractical and unlikely (you’d never see anything like this in a production app) function but it’s needed for the demonstration of main thread blockage.
The fetchUsers
function is then hooked to the Fetch Users Directly
button like this:
<button className="btn-direct" onClick={fetchUsers}>
Fetch Users Directly
</button>
With that done, let’s see what clicking on the button actually does.
The next thing to do is to import the Home.js
file into the main App.js
file so it can be referenced and then rendered on the UI. To do that, open up the App.js
file and edit it with the code block below.
import React, { Component } from "react";
import Home from "./Home";
import logo from "./logo.svg";
import "./App.css";
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<section className="App-intro">
<Home />
</section>
</div>
);
}
}
export default App;
In the code block above, Home.js
is imported and used in the render function like this; <Home />
.
So far, your app should look similar to the one below.
Now, if you click on the Fetch Users Directly button, you’ll notice that for almost two seconds, the action causes a blockage to the main thread. How? The whole app becomes unresponsive and the countdown timer stops working. This is an example of a process/function causing a blockage to the apps’ main thread.
Let’s see how we can solve this with Web Workers.
We’ll start first by creating the Web Worker file named worker.js
in the src
folder and edit with the code below:
As seen in the code block above, the Web Worker file also contains the same for
loop that was written in the fetchUsers
function. The for
loop is inside a message
event listener which means that the processing will be done as soon as it receives an event and at the end it sends a message (the users
array) via the postMessage
function.
With the Web Worker file created, let’s import it into the Home.js
file.Before we do that though, some config is needed before the worker.js
file can be referenced in the React app.
This code block is necessary because we’ll be importing the worker.js
therefore we need to make sure it’s Webpack compatible and the code block above does that by turning the Web Worker file into a path/string which can then be called as a web url.
Next, in the Home.js
file, add the following imports to the top of the code:
import worker from "./worker.js";
import WebWorker from "./workerSetup";
We’ll also need to initialize the Web Worker instance once the components are done mounting, in the componentDidMount
lifecycle:
componentDidMount = () => {
this.worker = new WebWorker(worker);
};
Remember, Web Workers are all about posting and receiving messages asynchronously, therefore, let’s write the function that posts the message to the Web Worker file and then in turn receives the returned message. You can write the function below just before the componentDidMount
lifecycle.
The postMessage is a simple one since we are only listening for one kind of event in the worker.js
file. The eventListener
then listens for the response from the Web Worker and then updates the state. The last bit of code to be written is hooking the fetchWebWorker
function to the Fetch Users With WebWorker
button.
<button className="btn-worker" onClick={fetchWebWorker}>
Fetch Users with Web Worker
</button>
Now if you visit the app, and try clicking on the Fetch Users With Web Worker
button, you’ll observe that the app carries on smoothly while the requested process is happening in the background and the state is then updated with the appropriate value.
You can test this out on the live app at https://build-jrrpxladjd.now.sh/.
Conclusion
Web Workers can be very useful for handling complicated processes and functions as demonstrated here and you should consider offloading long running tasks to a Web Worker file and then notify your main app when it’s done and you can then update whatever needs to be updated. The complete codebase for the React app above can be viewed on GitHub and also CodeSandbox.