Getting your Trinity Audio player ready...
|
The cause
When you write any JavaScript using the tag, usually it gets executed in the main thread. And by convention, we can say that it ( main thread ) is responsible for handling user events and paints. If your main thread is busy with some other job instead of working on handling the UI events, Obliviously you are going to end up with page jank and unresponsive UIs.
Am I saying that you shouldn’t write anything in the main thread then? If my million-dollar idea of a web app is generating a million-line CSV file for the user to download, where should I write that business logic?
Check out the demo here: https://melodic-narwhal-64e4d5.netlify.app/
Check the code block below from the demo application which lets the user upload an image and the application converts the image data to grayscale and displays it on a canvas.
const filePicker = document.getElementById("file_picker")
const canvas = document.getElementById("canvas")
const convertButton = document.getElementById('convert_btn')
const ctx = canvas.getContext('2d');
filePicker.addEventListener('change',openAndLoadFile)
convertButton.addEventListener('click', function(){
let imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
imgData = convertToGrayScale(imgData)
ctx.putImageData(imgData, 0, 0);
})
On the demo page, if you try processing a huge image, you can see that it takes some time for the image to get converted and it freezes the UI and takes some time for it to become responsive again. So this is a really clear example showcasing that if we are trying to overwork the main thread, it is almost certain to result in a terrible user experience.
If not main thread where?
There are several ways to do this. Obviously, you can do that on the backend (duh !!). But for some use cases, it will not be able to scale well enough. What if we could do some heavy processing on the client side itself without it affecting the page rendering performance?
One way to do that is by using web workers.
So what is a web worker?
Web Workers are a simple means for web content to run scripts in background threads. The worker thread can perform tasks without interfering with the user interface.
https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
Sounds alluring, isn’t it? So let’s explore
Web Workers
So basically web workers are a way to offload some work from the main thread and let it run in a separate thread. This way the main thread can continue its work.
Example
Think of a restaurant where just one person (you) does all the work. You approach the customer and ask “what would you like to eat?”. they will check the menu and give you their order, and you will go to the kitchen to prepare the meal and serve it to him.
So what will happen if another customer walks in while you are cooking? You cannot just walk away and take their order, What if 5 more customers walk in? See how inefficient it is.
So you hire a cook; now it’s the cook’s responsibility to make the meal. So what you will do is take the order, pass the order to the cook, and thereafter the preparation is going to be taken care of by the cook. The cook will let you know once it is ready to be served. Thus, while the cook is cooking you are free to tend to other customers or do any other stuff.
This is what web workers do. They just take messages from the JavaScript main thread and process them. Once it is done, it will contact the main thread with the results. So this frees the main thread from doing the heavy lifting and it can now focus on running the UI.
Creating a web worker
const myNewCook = new Worker('cook.js');
Where cook.js is a separate javascript file with the logic and functionality to process the message we pass to our worker
Continuing our previous example, won’t it be pretty awkward in our restaurant if our chef is running around in his apron and taking orders from customers? It’s similar to web workers as well. By design web workers aren’t allowed to interact directly. Workers have zero knowledge of the DOM whatsoever. This means they cannot access the HTML at all, nor access global variables. This is to prevent unpleasant things from happening if more than one thread attempts to manipulate the same data.
Telling a web worker to cook
In the restaurant example, we can pass notes to each other to communicate and let each other know what should be done. Similarly, there is an interface with which we can communicate with the web workers using postMessages
// main thread
// Send data / message to worker
myNewCook.postMessage('cook_pasta');
// receiving data / message from worker
myNewCook.onmessage = function({data}) {
if(data == 'cooked_pasta'){
serveCookedPasta()
}
}
// handle errors using `onerror`
myNewCook.onerror = function(){
// restaurant under fire
run()
}
//cook.js
onmessage = function({data}) {
if (data == 'cook_pasta'){
cook('pasta')
postMessage('cooked_pasta'); // once pasta is cooked
}
}
You can pass, any serialisable data between worker and the main thread using postMessage
Let us optimize then
So with what we have learned so far, let us try to optimize the application we created earlier
// main.js
// creating a new worker
const worker = new Worker('worker.js');
file_picker.addEventListener('change',openAndLoadFile)
convertButton.addEventListener('click', function(){
let imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
// sending the image data to worker
worker.postMessage(imgData)
worker.onmessage = function({data}) {
ctx.putImageData(data, 0, 0); // writing the image data back to our canvas
}
})
// worker.js
onmessage = function(e) {
console.log(e.data);
const data = convertToGrayScale(e.data)
postMessage(data);
}
check out the demo here: https://gleeful-arithmetic-1fda26.netlify.app/
Now you can see that, if you are processing the image via worker the UI is not freezing anymore.
The limitations
From what I have said so far you might be thinking ” web workers seem nice, let me add a few in my application. Few more threads ain’t gonna hurt“. Hold your horses, it’s not all rainbows and unicorns. Like everything else, there are obvious trade-offs for web workers as well.- Now your application has started running on multiple threads, so the cost and effort needed to debug issues have gone up as well.
- Sometimes it is difficult to share resources, it takes too much time and in final result might not look good. As you can only share simple javascript datatypes between the worker and the main thread.
-
Web workers use a relatively high amount of memory when compared to the main thread, so spawning too many web workers and not properly managing them can often lead to crashes.
Reference Links
- https://developer.chrome.com/blog/inside-browser-part3/
- https://www.youtube.com/watch?v=7Rrv9qFMWNM
- https://nitropack.io/blog/post/minimize-main-thread-work
- https://www.smashingmagazine.com/2021/06/web-workers-2021/
Senior software Engineer at Rently Architecture