A quick guide to JavaScript Web Workers

Table of contents

Recently, I have been diving deep into the world of JavaScript, trying to understand some fundamental concepts. One of the most fascinating things I discovered is Workers!
As we all know JavaScript is a single threaded language. All of the code that you would normally write all runs on a single thread. But what if you encounter a block of code or a function that requires heavy computation? An example of this would be processing an image or saving a huge chunk of data in background.
This would mean that the rest of the code will have to wait until that block of code has been finished executing. If this code is written on the client side like a website or a mobile app, then that would mean the entire UI would freeze and the page would be unresponsive.
Workers vs Promises
One would think that Promises can be used as a workaround for such problems. After all, that’s the entire point of asynchronous code, right?
WRONG! ABSOLUTELY WRONG!!
Promises are used for writing asynchronous code. The function is executed alongside other blocks of code. They are used when they are accessing external resources like reading a file or HTTP request.
When the task is completed, we get the result, and the promise is removed from the event loop. But a worker has its own event loop and runs separately in the background.
We can also say that Promises help us write concurrent code, while Workers help us achieve true parallelism in JavaScript.
Using WebWorkers
JavaScript Workers are used when the task is too heavy and demanding on the main thread. We offload this heavy task to a new thread which is usually executed on a separate CPU core. Through Workers, we enable true parallelism in JavaScript as we can have multiple threads running at the same time.
We can define a Worker in main.js
as follows and send data to it as follows:
const worker = new Worker('./worker.js');
// If you are using bundlers like Vite
const worker = new Worker(new URL('./worker.js', import.meta.url));
worker.postMessage(/* data */);
The work done in the new thread is specified in the worker.js
file
In worker.js
file, we can receive the data:
addEventListener('message', (ev) => {
const data = ev.data;
/*
operation with the data
*/
postMessage(result);
});
In the browser, we are provided with functions like postMessage
or addEventListener
which can be useful when communicating with different threads.
We can add another event listener to the worker
instance which will receive data from the worker thread back in the main.js
.
worker.addEventListener('message', (result) => {
// do something with the result
});
This results in a non-blocking execution of the main thread while our worker thread is doing the heavy lifting.
A practical example
Here is a simple app in Code Sandbox which demonstrates the power of Web Workers. We calculate the Factorial of a number, and we find the Fibonacci sequence at that number.
On scrolling down, there is a fun box that you can click and play around with! Through this box you can see the behaviour of the UI with and without Workers.
Entering small values does not make much difference. But entering large or very large values (> 10K), the difference is pretty clear. If not using workers, the UI freezes, and our fun box stops responding to our clicks. You can refresh the page if the page becomes unresponsive.
Feel free to play around with this sandbox and see what runs and breaks!
Conclusion
Worker threads is a powerful feature of JavaScript and if used wisely, can significantly speed up your applications. However, they can also be the reason for your application to crash!
Additionally, worker threads are also available in Node.js. This expands the possibility of writing multithreaded code in JavaScript for the server, resulting in a more robust and efficient server side logic and providing a better user experience.
Subscribe to my newsletter
Read articles from Harshit Pandit directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
