DEV Community

Piyush Goyani
Piyush Goyani

Posted on • Originally published at blog.thesourcepedia.org on

Dedicated and Shared Web Worker with Performance Testing

Overview

Web Workers are threads attached in Browser and run on the client side. It runs in a separate space from the main application thread. Simply It gives the ability to write multi-threaded Javascript applications. It is generally used to do heavy lifting tasks(in terms of computational time), e.g process huge data from API, calculations, batching of real-time chart data before drawing in the background.

Web Workers perform all processes in the Worker thread aka separate environment which is separate from the main JavaScript thread without blocking UI or affecting performance thus does not have access to DOM, window and document objects, and for the same reason, it can not simply manipulate DOM.

The life span of a Worker is limited to a browser tab, when a browser tab is closed Worker also dies. The data are passed between Worker and the main thread via messages. To send data postMessage() method is used and to listen to messages onmessage event is used on both sides.

A single web page/app can have more than one Web Worker regardless of the number of tabs. It can be Shared or Dedicated workers. We'll go through demo examples for both workers and use cases.

Demo

In this demo, there are two examples, in first you can test that the 50k array is being sorted with bubble sort algorithm with and without Web Worker(Dedicated) and see the difference between both. Second, you can test Shared Worker which is used by two client sources for similar functionality. Both workers use Network APIs for the processing which is made in Node/ExpressJS.

Demo Page

Dedicated Workers

Dedicated Workers can only establish a single connection. It can be initialized using the following syntax.

let worker = new Worker("worker.js");

// receive data from web worker sent via postMessage()
worker.onmessage = function (e) {
  console.log(e.data);
}

// send data to web worker
.postMessage("start")

Enter fullscreen mode Exit fullscreen mode

To receive data onmessage event is used and to pass data postMessage method is used. In this demo, we have used a Dedicated worker to Bubble Sort 50k length of array data. In UI there are two options for sorting with or without Web Workers.

Without Web Worker

When the user clicks the without web worker option, the script starts and gets data from the API of the 50k array and starts sorting in a main, during this you may see a frozen progress bar, and the rest things are stuffed like not able to select the text, the mouse pointer changes to the cursor and other UI blocking effects because it is performing a heavy task to sort a large array. When sorting is done you'll see processing time and an animated section regarding the process below.

Without Web Worker

With Web Worker

When the user clicks the web worker option, the worker initiate and gets data from the API of the 50k array and starts sorting in a separate thread, during this you can see a progress bar and no UI blocking like text-selection and cursor etc. Everything is smooth. When sorting Done you'll see processing time and an animated section regarding the process below.

With Web Worker

So Web Workers overcome this problem. Here is the index.html file.

<body>
    <div id="wrap">
        <div class="container">
            <div class="row pt-3">
                <div class="col">
                    <h2>Dedicated Worker</h2>
                    <div class="text-secondary h4">
                        Sorting Array with Bubble Sort(50k)
                    </div>
                    <div>
                        <button class="btn btn-large btn-primary" onclick="nonWebWorker();">Without Web Worker</button>
                        <button class="btn btn-large btn-success" onclick="withWebWorker();">With Web Worker</button>
                    </div>

                    <div id="progressbar" class="progress hide">
                        <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar"
                            aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"></div>
                    </div>
                    <div id="resultBox" class="hide bg-info rounded px-2">
                        <p class="muted">
                            Array sorted in:
                        </p>
                        <h1 id="timespent"></h1>
                        <p id="withoutWW" class="output hide">
                            As you can see, without Web Worker, your browser may be able to sort the 50K Array but you
                            can't work with your browser while sorting and also your browser won't render anything until
                            sorting ends, that's why you can't see the animated progress bar on the page.
                        </p>
                        <p id="withWW" class="output hide">
                            Your browser sorted 50K Array without any crash or lagging because your browser supports
                            Web Worker. When you do a job with Web Worker, it's just like when you run a program in
                            another thread. Also, you can see the animated progress bar while sorting.
                        </p>
                    </div>
                </div>
                <div class="col-1">
                    <div class="vr h-100">
                    </div>
                </div>
            </div>
        </div>
        <script src="./utils.js"></script>
</body>

Enter fullscreen mode Exit fullscreen mode

Dedicated Worker file worker.js.

onmessage = async function (e) {
  if (e.data[0] === "start") {
    let a = [];

    async function getData() {
      return fetch(`${e.data[1]}/getData`)
        .then((res) => res.json())
        .then((data) => {
          a = data;
        });
    }

    function bubbleSort(a) {
      let swapped;
      do {
        swapped = false;
        for (let i = 0; i < a.length - 1; i++) {
          if (a[i] > a[i + 1]) {
            let temp = a[i];
            a[i] = a[i + 1];
            a[i + 1] = temp;
            swapped = true;
          }
        }
      } while (swapped);
    }
    let start = new Date().getTime();
    getData()
      .then(() => {
        bubbleSort(a);
      })
      .then(() => {
        let end = new Date().getTime();
        let time = end - start;
        postMessage(time);
      });
  }
};

Enter fullscreen mode Exit fullscreen mode

Shared Workers

The Shared Worker can establish multiple connections as they are accessible by multiple scripts even in separate windows, iframes(demo) or workers. It can be spawned using the below syntax.

let sharedWorker = new SharedWorker("shared-worker.js");
sharedWorker.port.postMessage("begin");
sharedWorker.port.onmessage = function (e) {
  console.log(e.data)
}

Enter fullscreen mode Exit fullscreen mode

In Shared Worker similar concept apply as Worker for data passing but via port object(explicit object) which is done implicitly in dedicated workers for communication. In this demo, there is a Shared Worker which does multiply/square of numbers. It is used in two places. The first is on the main page and the second is on another HTML page which is included in the main page via IFRAME. On the main page user input two number for multiplication and passes them to Worker and get multiplied output as below.

Shared Workers Main

In the second case, the user inputs single number input which is given to the same Shared Worker and gets a squared input number as a result. Now within the worker, it takes input as the number and calls API to do Math Operation and returns the result as below.

Shared Workers iFrame

Main HTML file to load IFRAME index.html.

<div class="col">
                    <h2>Shared Worker</h2>
                    <div class="text-secondary h4">
                        Multiply/Square Numbers with Shared Resource
                    </div>
                    <div>
                        <input type="text" id="number1" class="form-control" placeholder="Enter number 1" />
                        <input type="text" id="number2" class="form-control mt-1" placeholder="Enter number 2" />
                        <input type="button" class="btn btn-dark mt-2" value="Calculate" onclick="multiply();" />
                        <p class="result1 text-success pt-2"></p>
                        <iframe id="iframe" src="shared.html"></iframe>
                    </div>
</div>

Enter fullscreen mode Exit fullscreen mode

Second HTML file shared.html which load in IFRAME in parent.

<body>
    <script type="text/javascript">
        const endpoint = window.origin;
        if (typeof (Worker) === "undefined") {
            alert("Oops, your browser doesn't support Web Worker!");
        }

        function getSquare() {
            let third = document.querySelector("#number3");
            let squared = document.querySelector(".result2");

            if (!!window.SharedWorker) {
                let myWorker = new SharedWorker("shared-worker.js");
                myWorker.port.postMessage([third.value, third.value, window.origin]);
                myWorker.port.onmessage = function (e) {
                    squared.textContent = e.data;
                };
            }
        }
    </script>
    <div class="container">
        <h1>
            Shared Web Worker(iframe)
        </h1>
        <div class="row">
            <div class="col">
                <input type="text" id="number3" class="form-control" placeholder="Enter a number" />
                <input type="button" id="btn" class="btn btn-primary" value="Submit" onclick="getSquare()" />
                <p class="result2"></p>
            </div>
        </div>
    </div>
</body>

Enter fullscreen mode Exit fullscreen mode

Here is shared-worker.js file.

onconnect = function (e) {
  let port = e.ports[0];

  port.onmessage = function (e) {
    fetch(`${e.data[2]}/multiply?number1=${e.data[0]}&number2=${e.data[1]}`)
      .then((res) => res.json())
      .then((data) => {
        port.postMessage([data.result]);
      });
  };
};

Enter fullscreen mode Exit fullscreen mode

Utility functions file utils.js that handles worker-related stuff.

const endpoint = window.origin;
if (typeof Worker === "undefined") {
  alert("Oops, your browser doesn't support Web Worker!");
}

function nonWebWorker() {
  cleanWindowAndStart();
  let a = [];
  async function getData() {
    return fetch(`${endpoint}/getData`)
      .then((res) => res.json())
      .then((data) => {
        a = data;
      });
  }

  function bubbleSort(a) {
    let swapped;
    do {
      swapped = false;
      for (let i = 0; i < a.length - 1; i++) {
        if (a[i] > a[i + 1]) {
          let temp = a[i];
          a[i] = a[i + 1];
          a[i + 1] = temp;
          swapped = true;
        }
      }
    } while (swapped);
  }

  let start = new Date().getTime();
  getData()
    .then(() => {
      bubbleSort(a);
    })
    .then(() => {
      let end = new Date().getTime();
      let time = end - start;
      afterStop(time, false);
    });
}

function withWebWorker() {
  cleanWindowAndStart();
  let worker = new Worker("worker.js");
  worker.onmessage = function (e) {
    afterStop(e.data, true);
  };
  worker.postMessage(["start", endpoint]);
}

function cleanWindowAndStart() {
  $("#resultBox").hide(500);
  $("#withWW").hide();
  $("#withoutWW").hide();
  $("#progressbar").addClass("d-flex").show(500);
}

function afterStop(spentTime, mode) {
  $("#timespent").html(spentTime + "ms");
  $("#progressbar")
    .hide(500, function () {
      mode ? $("#withWW").show() : $("#withoutWW").show();
      $("#resultBox").show(500);
    })
    .removeClass("d-flex");
}

function multiply() {
  let first = document.querySelector("#number1");
  let second = document.querySelector("#number2");

  let multiplied = document.querySelector(".result1");

  if (!!window.SharedWorker) {
    let myWorker = new SharedWorker("shared-worker.js");

    myWorker.port.postMessage([first.value, second.value, endpoint]);

    myWorker.port.onmessage = function (e) {
      multiplied.textContent = e.data;
    };
  }
}

Enter fullscreen mode Exit fullscreen mode

Node/Express API server.js

app.get("/getData", (req, res) => {
  res.send(
    Array(50000)
      .fill(0)
      .map(() => Math.floor(Math.random() * 100))
  );
});

app.get("/multiply", (req, res) => {
  const multiply = req.query.number1 * req.query.number2;
  const result = isNaN(multiply) ? "Invalid input" : multiply;
  res.send({ result });
});

Enter fullscreen mode Exit fullscreen mode

Performance Testing

Now time for numbers. Let's test our Sorting demo with Chrome Performance Test in Browser DevTools. We will record and profile both with and without web workers so we can differentiate performance and resource utilization.

Without Web Worker

Open Dev Tools and navigate to the Performance tab and start recording. Once recording starts in UI click on the Without Worker button in Dedicated Worker Section and waits till sorting is done. Once you see the result stop recording and the preview will be there as below.

Chrome Perf Profile - Without Worker

This is an overview of the Performance tab which looks a little complicated. We'll use some of these for over understanding of the use case. In the first section, you can see the Frame rate, Network, CPU and Memory utilization chart. Below is the Network section where requests are recorded with the timeline. Below is Main Section which shows all tasks, macro tasks, functions and thread-related information that was executed in the tab during that time. You can click for a detailed view of each. Below is the CPU activity breakdown in a pie chart, which show the type of tasks with the time taken.

Chrome Perf Profile - Without Worker ActivityThis is a detailed activity view of the bubbleSort script, and here you can see that this function consumes most of the resources and time in a thread(96.5% - 5663 ms) and other processes like rendering dom and manipulation, network calls consumed rest of all. You can save your profile if you want or delete it.

With Web Worker

Now once you test with Without Worker Profiling, start with With Worker button. The process is the same, start recording -> click on With Web Worker button -> stop recording once sorting is done. Before that make sure the Performance tab is cleared and you will see a similar result as below.

Chrome Perf Profile - With Worker

When you look in the profile With Workers in Performance tab you'll see a significant difference from the previous summary of the consumed time by task type. Here the script is consume very less time.

Chrome Perf Profile - With Worker Activity

Again, with a worker as you can see in activity details, you won't see bubbleSort or any other script taking a long time this breakdown is of the main thread but sorting is done in a worker thread so it doesn't affect the main thread resources.

Advantages & Limitations with Usecases

  • Can be used to perform CPU-intensive tasks without blocking UI.
  • Has access to fetch, so can communicate over Netowork to make API requests.
  • Doesn't have access to DOM, so can't manipulate therefore, tasks like canvas, images, SVG, video or any element-related drawing/direct manipulation is not possible.
  • Can be used in Real-time data processing in the Stock market and related fields e.g Crypto, and NFT.
  • To process large data-sets like DNA and Genetics
  • Media manipulation in the background e.g image compress/decompress, video etc.
  • Can be used to process textual data in the background e.g NLP
  • Caching: Prefetching data for later use

Conclusion

In this article, we have got the basic idea of Web Workers including Dedicated and Shared Workers and How they affect user experience if used properly in a project. We also debug it with the Chrome DevTools Performance tab as proof of the consumed resources that our app consumed.

If you enjoyed the article and found it useful give it a thumb. Let us know your thoughts on this in the comment section below. You can find me on Twitter

Top comments (0)