React: Web Workers to the rescue
Tags: react, webdev, javascript
Published: 2020-12-29 10:30:22

One week ago I tried to create a small tool with React where I could upload an image and this would be encoded to a [BlurHash](https://blurha.sh/) string. After setting up the main functionality, previewing the original image and the blured, I moved to the encoding part. It worked but I noticed a slight issue. When the app was encoding it was becoming unresponsive and therefore until the encoding finished unusable. I tried to mitigate this issue and provide a better UX experience by adding spinners and disabling every possible interaction until the process was finished. Also the sole purpose of this tool is to do the encoding so you don't expect to do something other than that in the meantime.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/iqbk4ejbf5rt3ck0iszs.png)

But this made me curious, how could I tackle this issue, what if in the future I wanted to add another feature in my app where the user wanted to interract with my application while it was doing some heavy calculations? And here come the Web Workers. I' ll try to explain how it worked for me in the context of React and CRA (Create React App) and how it helped me solve my problem.

### What is a Web Worker

Quoting from [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers): 


"Web Workers are a simple means for web content to run scripts in background threads."

Javascript is single-threaded, meaning it has only one call stack and one memory heap, it executes code in order and must finish executing a piece of code before moving to the next one. So this is where the problem lies, that until the encoding of the image finishes the UI can't execute any other "piece" of code. So if we can move the responsibility of encoding to a Web Worker the main thread will be free to handle user inputs.


### Setup React App

If you are using CRA for starting your project you need firstly to do some steps as CRA has no "native" support for Web Workers.

In order to use Web Workers we need to update our webpack config and add [worker-loader](https://www.npmjs.com/package/worker-loader), but tweaking webpack in apps created with CRA is not possible without using [react-app-rewired](https://www.npmjs.com/package/react-app-rewired) a module that gives you the ability to 

"Tweak the create-react-app webpack config(s) without using 'eject' and without creating a fork of the react-scripts."

So we install both of those dependencies and then we create a file `config-overrides.js` where we can override webpack and add `worker-loader`.

```javascript
module.exports = function override (config, env) {
  config.module.rules.push({
    test: /\.worker\.js$/,
    use: { loader: 'worker-loader' }
  })
  return config;
}
```

| Keep in mind your Web Worker script needs to have a name on `.worker.js` format.

Finally we need to make sure our `package.json` scripts call `react-app-rewired` instead of `react-scripts`

```json
"scripts": {
  "start": "react-app-rewired start",
  "build": "react-app-rewired build",
  ...
}
```

Now you are ready to use Web Workers in a React app created with CRA.

### How it looked

So let's try and have a look on some code and how to solve the issue of blocking UI during heave calculations.

My code looked something like this

```javascript
useEffect(()=>{
...
  encodeImageToBlurhash(url,x,y)
    .then()
    .catch();
...
},[url,x,y]);
```
and the `encodeImageToBlurhash` was loading an image from a canvas and calling the "costly" `encode` function.

```javascript
async function encodeImageToBlurhash (imageUrl,x,y) {
  const image = await loadImage(imageUrl);
  const imageData = getImageData(image);
  return encode(imageData.data, imageData.width, imageData.height, x, y);
};
```

### Refactoring

After the refactoring my code looked like

```javascript

useEffect(()=>{
 let worker;

 async function wrapper() {
    worker = new EncodeWorker();

    worker.addEventListener('message', (e)=> {
      const { hash } = e.data;
      ...
    });

    worker.addEventListener('error', e => {
      console.error(e);
      ...
    });

    const [data, width, height] = await 
    encodeImageToBlurhash(url);

    worker.postMessage({ payload: { data, width, height, x, y } 
    });
  }

  wrapper();

  return () => { if(worker) worker.terminate();}
},[...]);

```

and the `encodeImageToBlurhash` just returns the image data now

```javascript
async function encodeImageToBlurhash (imageUrl) {
  const image = await loadImage(imageUrl);
  const imageData = getImageData(image);
  return [imageData.data, imageData.width, imageData.height];
};
```

A lot of code here but I am going to explain. 


So useEffect changed and now:

- Creates a Web Worker, 
- Added listeners for `error` and `message`, as Web Workers communicate with the code that created them with event handlers and posting messages, 
- Call the `encodeImageToBlurhash` to get the image data, 
- call the "costly" encode function from inside the Web Worker by posting the image data in order to start the calculations
- and finally terminate the Web Worker.


Our Web Worker is not really complicated 

```javascript
const ctx = self;
const { encode } = require('blurhash');

ctx.addEventListener("message", (event) => {
  const { payload } = event.data;
  const hash = encode(payload.data, payload.width, payload.height, payload.x, payload.y);
  ctx.postMessage({ hash });
});
```
as it's just listens for a message and starts encoding the image data and after it finishes reports back with the resulting string.

### Result

Now the result is every time we do a calculation we create a Web Worker that runs on a different thread and leaves the main thread, where UI runs unblocked and ready to accept user input.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/hvpznpnhuo4bon3ezpvx.png)

and as you can notice now we have the loaded Web Worker, and a second thread running other than `Main`.

### Resources

- [Using Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers).
- Also you can find the actual code used in [Github](https://github.com/gkampitakis/blurhash-previewer) with more details.