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 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.
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:
"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, but tweaking webpack in apps created with CRA is not possible without using 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
.
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
"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
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.
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
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
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
andmessage
, 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
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.
and as you can notice now we have the loaded Web Worker, and a second thread running other than Main
.
Resources
- Using Web Workers.
- Also you can find the actual code used in Github with more details.
Top comments (2)
This is so crucial yet such an uncharted territory.
Thank you so much for explaining it so clearly!
Good stuff!