Multithreading In JavaScript using Web Workers
Tags: javascript, performance, webdev, designpatterns
Published: 2022-08-15 10:38:40

<p>Have you ever heard that Javascript is a single-threaded language and can only execute one process at a time well that is completely true. JavaScript uses an event loop to execute the process, for now, think of the event loop is as a queue where all the Javascript processes kept inside and are executed one by one.</p>
<p>In older Browsers, the whole browser was executed on Only one thread but now Each tab of the browser is given a thread to execute the process on.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1660493194715/tf0OFi3hT.png" alt=" Javascript EventLoop" /></p>
<h1 id="heading-problem-with-single-threaded-programing-language">Problem with Single threaded Programing Language.</h1>
<p>As JavaScript is a single-threaded Programing language so now only one Process can be executed in one tab. You will have a better understanding of this if you are familiar with Event Loop in Javascript.</p>
<p>For normal purposes, It is more than enough but for the CPU-intensive process, this can cause major issues in the UI. Suppose you want to do a certain CPU-intensive task and while doing it another process will stop working such as clicking or scrolling or anything else you do on the website.</p>
<p>Here is an example where I calculate a sum of all the numbers in the range using a for loop. </p>
<pre><code>let sum <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
  <span class="hljs-keyword">for</span> (let i <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i <span class="hljs-operator">&lt;</span> <span class="hljs-number">10000000000</span>; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
    sum <span class="hljs-operator">+</span><span class="hljs-operator">=</span> i;
  }
  alert(<span class="hljs-string">"The sum is: "</span> <span class="hljs-operator">+</span> sum);
</code></pre><p>Let's see what happens in the browser if I run this.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1660492675114/TkQBEpHXX.gif" alt="webWorker.gif" /></p>
<p>let's understand what happening here In this Gif. Here are two Buttons one is to change the background color of the Website and one is to do CPU-intensive work which is to calculate the Sum and once done it just shows the sum on the screen as the gif.</p>
<p>Now before I clicked on the Calculate sum button I was able to change the background of the Website but once I click on the Calculate Sum the javascript will only execute any other process once it calculate the other. As executing the calculated Sum takes a lot of time it just freezes all the other processes.</p>
<p>Well, this can be solved by using something called web workers.</p>
<h1 id="heading-modern-problem-required-morden-solution">Modern Problem Required Morden Solution</h1>
<h2 id="heading-an-introduction-to-web-workers">An Introduction to Web Workers</h2>
<p>MDN in their official document says and I quote</p>
<blockquote>
<p>Web Workers are a simple means for web content to run scripts in background threads.</p>
</blockquote>
<p>Meaning you can now run your Javascript Script on a different thread in the background. Which is exactly the solution we are looking for.</p>
<p>Let's try to do that using a web worker, as a web worker needs a new script so will create a new script for that let's call that a <code>worker.js</code>, and in our <code>main.js</code> let's initiate the worker script using the single line <code>new Worker("worker.js")</code>this is how easily a new worker can be initiated in your already written javascript.</p>
<p>Our main.js will look something like this </p>
<pre><code><span class="hljs-comment">// main.js </span>

const calculateSum <span class="hljs-operator">=</span> document.getElementById(<span class="hljs-string">"calculateSum"</span>);
const changeColor <span class="hljs-operator">=</span> document.getElementById(<span class="hljs-string">"changeColor"</span>);

const worker <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> Worker(<span class="hljs-string">"worker.js"</span>);
calculateSum.addEventListener(<span class="hljs-string">"click"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{

<span class="hljs-comment">// We posted a message to the worker with a simple JSON as we don't require to pass any data</span>
  worker.postMessage({ text: <span class="hljs-string">"sum"</span> });

<span class="hljs-comment">// Our worker is listening to the return message then it will execute some logic on the basis of data passed.</span>
  worker.onmessage <span class="hljs-operator">=</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">e</span>) </span>{
    document.getElementById(<span class="hljs-string">"sum"</span>).innerHTML <span class="hljs-operator">=</span> `the sume <span class="hljs-keyword">is</span> ${e.data}`;
    worker.terminate() <span class="hljs-comment">//terminatiing the worker after the use</span>
  };
});

changeColor.addEventListener(<span class="hljs-string">"click"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  document.body.style.backgroundColor <span class="hljs-operator">=</span>
    document.body.style.backgroundColor <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-string">"red"</span> ? <span class="hljs-string">"blue"</span> : <span class="hljs-string">"red"</span>;
});
</code></pre><p>now as you can see I have moved the logic to calculate the sum from the main.js to the worker.js</p>
<pre><code><span class="hljs-comment">// worker.js</span>

onmessage <span class="hljs-operator">=</span> (e) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
  let sum <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
  <span class="hljs-keyword">for</span> (let i <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i <span class="hljs-operator">&lt;</span> <span class="hljs-number">10000</span>; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
    arr[i] <span class="hljs-operator">=</span> i;
  }
  postMessage(sum);
};
</code></pre><p>this will post the sum back and the Website will work without any blockage. You can easily try it.</p>
<h1 id="heading-things-to-remember-while-using-web-worker">Things to remember while using Web-Worker</h1>
<p>Even though workers are amazing but there are some trade-offs which we will talk about in this Section.</p>
<blockquote>
<p>With great power come's the great Responsibility </p>
</blockquote>
<p>Web Workers use message passing which is performance costly. The message is copied instead of share which is exactly the reason for the performance cost.</p>
<p>Let's use <code>performance.now()</code> web API to get the performance of the same function in the main thread and web-worker thread and let's watch the performance.</p>
<p>I changed the code according to our use case. Here are the new codes </p>
<pre><code><span class="hljs-comment">// main.js</span>

const calculateSum <span class="hljs-operator">=</span> document.getElementById(<span class="hljs-string">"calculateSum"</span>);
const changeColor <span class="hljs-operator">=</span> document.getElementById(<span class="hljs-string">"changeColor"</span>);

const worker <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> Worker(<span class="hljs-string">"worker.js"</span>);

threadedFunction <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
  worker.postMessage({ text: <span class="hljs-string">"sum"</span> });

  worker.onmessage <span class="hljs-operator">=</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">e</span>) </span>{
    <span class="hljs-comment">// document.getElementById("sum").innerHTML = `the sume is }`;</span>
    worker.terminate();
  };
};

MainThread <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
  let start <span class="hljs-operator">=</span> performance.now();
  let sum <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
  let arr <span class="hljs-operator">=</span> [];
  <span class="hljs-keyword">for</span> (let i <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i <span class="hljs-operator">&lt;</span> <span class="hljs-number">10000</span>; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
    arr[i] <span class="hljs-operator">=</span> i;
  }
  let end <span class="hljs-operator">=</span> performance.now();
  console.log(`Main thread took ${end <span class="hljs-operator">-</span> start} ms to complete`);
};

calculateSum.addEventListener(<span class="hljs-string">"click"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  MainThread();
  threadedFunction();
});

changeColor.addEventListener(<span class="hljs-string">"click"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  document.body.style.backgroundColor <span class="hljs-operator">=</span>
    document.body.style.backgroundColor <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-string">"red"</span> ? <span class="hljs-string">"blue"</span> : <span class="hljs-string">"red"</span>;
});
</code></pre><pre><code><span class="hljs-comment">// worker.js</span>

onmessage <span class="hljs-operator">=</span> (e) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
  let start <span class="hljs-operator">=</span> performance.now();
  let arr <span class="hljs-operator">=</span> [];
  let sum <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
  <span class="hljs-keyword">for</span> (let i <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i <span class="hljs-operator">&lt;</span> <span class="hljs-number">10000</span>; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
    arr[i] <span class="hljs-operator">=</span> i;
  }
  postMessage(arr);
  let end <span class="hljs-operator">=</span> performance.now();
  console.log(`Worker took ${end <span class="hljs-operator">-</span> start} ms to complete`);
};
</code></pre><p>Here are the results for the above benchmarks on my Pc with an i7 processor.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1660503911754/8qx3akRIQ.png" alt="image.png" /></p>
<p>Well, this might come as a shock to you because we are actually running the same code than why it is almost 2x slow.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1660523647501/x6EFWgPQ_.png" alt="image.png" /></p>
<h2 id="heading-debugging-the-performance">Debugging the Performance</h2>
<p>Let's use the performance profiler to actually see what is taking so long for it to execute the same program in the worker thread.</p>
<p>As you can see the message takes almost 0.5ms whereas the original function call only took 1.5ms. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1660504475768/4WJjjIKgy.png" alt="OnMesage Time" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1660504492117/d3yqd3pBc.png" alt="Function Call" /></p>
<p>In Web Worker the more complex the data you pass the more time it will take to pass as it copies the whole data.</p>
<p>There are various ways to optimize the performance which we will talk about in the next session.</p>
<h1 id="heading-tldr">TL;DR</h1>
<ol>
<li>Javascript is a Single Threaded Language but you can use multithreading in javascript using web-workers in the browser.</li>
<li>Web-Workers are easily initiated using <code>new Worker()</code> and you can pass data from the main thread and worker thread using <code>onmessage()</code> method and <code>postMessage()</code> method.</li>
<li>Complex Data takes more time to pass as workers do not share the data but instead copy them.</li>
</ol>
</br><p style="color:#808080;">This blog is posted using <a target="_blank" href="https://blogtiple.com">Blogtiple</a></p>