Multithreading on web
Hello awesome readers, hope you are having a great time. Today we will discuss a performance issue on the web and how to solve it using workers and multi-threading. Before we start, let's understand what is a thread and how it works?
UI thread (aka Main thread)β
Each unit of code capable of executing independently is called a thread. The main thread is where a browser processes user events and paints. By default, the browser uses a single thread to run all the JavaScript.
To make it more simple, we can say all the synchronous tasks are executed on the main thread.
For example:
const var1 = 5;
const var2 = 6;
const result = var1 + var2;
console.log(result);
The above code executes very fast and does not block the main thread. Now let's see what happens when we try to perform a heavy task.
index.html<body>
<button id="less">iterate till 1000</button>
<button id="more">iterate till 10000000000</button>
<script src="main.js" />
</body>
let lessWork = document.getElementById('less');
let moreWork = document.getElementById('more');
const work = (iteration) => {
console.log('start');
performance.clearMeasures();
performance.mark('start');
let i = 0;
while (i < iteration) {
i++;
}
performance.mark('end');
performance.measure('Work', 'start', 'end');
console.log(
`took ${performance.getEntriesByType('measure')[0].duration} to finish ${iteration} iterations`
);
};
lessWork.addEventListener('click', () => {
work(1000);
});
moreWork.addEventListener('click', () => {
work(10000000000);
});
Result
In the above example, we can see the second task is taking a lot of time to complete. Users can not interact with the page, since everything is frozen on the screen until the iteration is completed.
It shows a long-running synchronous task blocks everything on the page like, user events, and rerender phase.
Now there is a quick question π€
If Javascript is single threaded then how is it asynchronous?β
const work = (noOfIteration) => {
for (let i = 0; i < noOfIteration; i++) {}
};
const lessWork = () => {
work(100);
console.log('less Work done');
};
const moreWork = () => {
work(100000000);
console.log('more Work done');
};
lessWork(100);
setTimeout(() => moreWork(10000000));
lessWork(100);
less Work done // almost immediately
less Work done // almost immediately
more Work done // after a few seconds
Although we managed to unblock the 2nd lessWork
, when the timer elapsed, moreWork
came back to the main thread and blocked it for some time. Here is a visualization for the above example.
Making a task asynchronous does not help to unblock the main thread, rather it moves the task to the point when the main thread is free. But in the end, the task will be executed synchronously. All the magic is done by the Event Loop behind the scenes
Okay, so first we said that Javascript is single threaded, Now we are saying Asynchonocity does not help to unblock the main thread. Then how to tackle this problem??
Javascript is single threaded but the browser is notβ
Before we jump into the solution, Let's observe this browser behavior.
We know the browser maintains a single javascript thread. But at the same time, if we look at it in a different way, Browser maintains a separate and unique thread for each tab session. That means the browser is capable to handle multiple threads. If somehow Javascript could communicate to the browser to create a new thread and process some data, then our problem can be solved.
Workers to Rescueβ
Mainly we have 3 types of workers:
- Web workers
- Service workers
- Shared Workers
In this particular scenario, Web workers can help us to solve this issue.
What is a web worker?β
A web worker is a web API provided to execute a given script in a worker thread. Web workers are able to utilize multi-core CPUs more effectively. It runs outside of the HTML context, therefore doesn't have the access to DOM.
It is a different thread from the Main thread and actually allows us to multithreading on the web.
How to create a web worker?β
const myWorker = new Worker('path/to/worker.js');
Let's update the previous example to make use of workers.
index.html<body>
<button id="more">iterate till 10000000000</button>
<script src="worker.js" />
<script src="main.js" />
</body>
self.onmessage = function (e) {
console.log('Message received from main script');
let i = 0;
while (i < e.data) {
i++;
}
console.log('Posting message back to main script');
self.postMessage(i);
};
let moreWork = document.getElementById('more');
// worker thread created
const worker = new Worker('worker.js');
moreWork.addEventListener('click', () => {
// sent message to worker
worker.postMessage(10000000000);
console.log(
'this will be printed immediately as main thread is not blocked'
);
// listen to the response from worker
worker.onmessage = function (e) {
// async callback
console.log('Message from worker - ', e.data); // Message from worker - 10000000000
};
});
Surprisingly, now when we click on the button, UI doesn't freeze. We can interact with other parts of the page (In this case page is blank though π). Let's see what is happening under the hood
As we can see in the above diagram, the blocking task is moved to another thread for doing all the heavy lifting. Once the task is processed, the main thread will receive a message from the worker.
Communication between the main thread and the worker threadβ
There are two simple methods available to do this communication.
- postMessage (i.e. the outgoing call)
worker.postMessage(payload); // main to worker
self.postMessage(payload); // worker to main - onmessage (i.e. the incoming call)
** Please notice
worker.onmessage = function(e) {...} // inside main
self.onmessage = function() {...}; // inside workerm
inonmessage
is in lowercase.
Real life applications of Web Workersβ
- image processing (like adding filters, applying stickers)
- generating UUIDs
- canvas Animation
Limitations of Web workersβ
- A worker canβt directly manipulate the DOM
- It has limited access to the methods and properties of the
window
object. - A worker can not be run directly from the filesystem. It can only be run via a server/browser environment.
Conclusionβ
In this article, We also got a glimpse of multithreading on the web and its potential to boost performance. In the upcoming articles, we will dig deeper into web workers and service workers and explore them more.
Thank you so much for your time. I will be happy to receive feedback. If you liked my posts, Please give a reaction. Have a great day ahead !!
Lalit Kanta Dibyadarshan
Frontend developer #twBloggersClub