Skip to main content

Multithreading on web

Β· 6 min read
Lalit Dibyadarshan
Front End Engineer @ Thoughtworks

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>
main.js
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 block thread.gif

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.

shockingg.webp 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);
Result
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.

Untitled-2022-07-20-1043.png

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:

  1. Web workers
  2. Service workers
  3. 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>
worker.js
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);
};
main.js
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

worker thread.png

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.

  1. postMessage (i.e. the outgoing call)
    worker.postMessage(payload); // main to worker
    self.postMessage(payload); // worker to main
  2. onmessage (i.e. the incoming call)
     worker.onmessage = function(e) {...} // inside main
    self.onmessage = function() {...}; // inside worker
    ** Please notice m in onmessage 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