DEV Community

Cover image for The Web Has Threads? Building Super-Charged Parallel Web Applications.
sk
sk

Posted on • Updated on

The Web Has Threads? Building Super-Charged Parallel Web Applications.

On average it's takes a human person tenth of a second(100 milliseconds) to blink.

Which is to fully close and open their eyes.

The browser's main thread has one-sixth of a human blink(100 milliseconds),
approximately 16.67 milliseconds to paint a flawless frame.

The main thread is overworked and underpaid , can we help it?

Web workers have been around for years, a way for browsers to spawn a separate thread.

Allowing the main thread to shine at what it does best - painting the browser.

source code: git

Web Worker: introduction

Working with threads is not as hard as it sounds!
and I am willing to put my head on a block and say in most high level languages.

To most beginners the word thread is ominous, at least it was for me.

Most high level languages provide beautiful abstractions,

unless you are working on a complex project, you will never encounter most thread associated problems.

Having created an internal data frame for a company based on web threads, I am happy to report,

I haven't encountered a single one from that list, the browser is very well abstracted!

The only practical, which we will develop a simple solution for, is establishing a communication pattern.

Create a simple HTML , CSS and JavaScript project, I use the vscode live server plugin to serve the project.

live server vs code plugin


src\
   app.js
   thread.js  
index.html 


Enter fullscreen mode Exit fullscreen mode

index.html starter:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Threads</title>
</head>
<body>

    <div style="width: 100%; display: grid; place-content: center; padding: .8em;">
        <button id="btn">fetch</button>
    </div>
    <div class="app">

        <div class="container">

        </div>


    </div>
   <script type="module" src=".\src\app.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Let's ignore the fetch button and container for now, they will become relevant later, when use the pokemon API, with workers.

In app.js let's spawn a thread, in the web: workers is the official name for threads, web workers.
Saying threads, is way easier and sounds way cooler, you can use any.

// app.js
 document.addEventListener("DOMContentLoaded", ()=> {
       // new URL means read\load thread.js relative to us the app.js file
       // not our website URL, this case localhost
      const worker = new Worker(new URL("./thread.js", import.meta.url))
  })
   
Enter fullscreen mode Exit fullscreen mode

The new keyword is always associated with constructing, constructors,

we are constructing an object, on top of spawning a separate thread,

new Worker runs thread.js in the spawned thread, while also returning an object

Having properties describing the thread and functions for interacting with and between threads.

Add a console.log so we can see when the thread is being spawned, as it immediately run's thread.js:

console.log("Thread running.......")
Enter fullscreen mode Exit fullscreen mode

Our interest at the moment is communication/passing data:

meet postMessage and onmessage.

postMessage and onmessage

To exchange data\messages between the threads we use postMessage

From the main thread to the worker:

// we user the constructed worker object
// app.js

 document.addEventListener("DOMContentLoaded", ()=> {
      const worker = new Worker(new URL("./thread.js", import.meta.url))
       worker.postMessage()
  })
   
Enter fullscreen mode Exit fullscreen mode

postMessage takes two parameters

data/message - a value to passed to the concerned thread,
it can be any JavaScript value, copied using structured cloning.

transfer - an optional array of transferrable objects , to transfer ownership of an object from one thread to another,

we will not cover transferrable objects in this article.

Passing a string to the worker:

   document.addEventListener("DOMContentLoaded", ()=> {
       ... 
       worker.postMessage("hello from main")
  })
   

Enter fullscreen mode Exit fullscreen mode

To pass data from the worker we do the same, but instead of using a constructed object,

we use self:



console.log("Thread running.......")

self.postMessage("hello from worker thread")
Enter fullscreen mode Exit fullscreen mode

Now we need to catch these broadcasts, which is where onmessage come's in:

In main let's listen for a worker message:

  document.addEventListener("DOMContentLoaded", ()=> {
       ... 
      
       // listening for the message event 
       // the passed message will be on e.data
       worker.onmessage = e => {
         console.log(e)
       }
       
      worker.postMessage("hello from main")
  })
  
Enter fullscreen mode Exit fullscreen mode

In the worker :



console.log("Thread running.......")


self.onmessage = e => {

  console.log(e)
}

self.postMessage("hello from worker thread")
Enter fullscreen mode Exit fullscreen mode

Simple right?, yeah this is where we meet our first and hopefully last pain point, when we pass a message to a thread,

there are few things we need to know, e.g what the message is?, what to do with it? was there an error? etc, this is the communication problem I pointed out earlier.

we need a way to encode this information, each thread needs to know what's happening on either side.

Having worked with workers quite a bit,

I settled on simple event driven system, that is extensible, which you can make as complex as you need

The communication problem

The solution inspired by the event driven architecture, w/o all the complexities of adapters and so on.

Objects are GOATED in JavaScript, they are dynamic, can take any shape, a bonus O(1) - instant access.
The idea is simple, objects are the only data we allow ourselves to pass.

This makes it possible to send any form of data, to any thread, the only condition being, we have a few reserved keys: event, isError, Error

meaning any thread should expect an object with these keywords by default:

  • event - telling the thread what to do.
  • isError - telling the concerned thread there's was an error completing the request
  • Error - returns that error

This way all the threads will always be in sync with each other's status, an example will do more justice than explanation.

The first thing we do is declare a global object,

const EventObject = {event: "",isError: false, Error: null} 
Enter fullscreen mode Exit fullscreen mode

When we post a message, we override or add properties to this object, as required for our use case,

worker.postMessage({...EventObject, ...{event: "pokemonFetch"}})
Enter fullscreen mode Exit fullscreen mode

Thus creating a new object from the given,

In every thread now we know, that the event property controls the flow, Which makes event handling even better,

That is literally a switch statement:

self.onmessage = e => {
    // extracting data from event, and event, isError and Error from data
    const {data: {event, isError, Error}} = e 
       if(!isError){
          switch(event){
           // relevant computation here
              }
        }else{
        // handle error
            console.error(Error)
        }

    }
Enter fullscreen mode Exit fullscreen mode

All this will come together and be solidified in the pokemon app example.

Pokemon API example

This part is, to solidify the concepts we have learned so far, in a 'real' scenario.

We will fetch data from an API, using a thread, then passing the data to the main thread.

Main will only handle constructing the UI from the given data, this part will move a bit faster, as we have covered all the fundamentals.

Fetch

navigate to app.js

Remember the container and the fetch button we skimmed over in beginning, we are back to them, the button will trigger a fetch request to the PokeAPI

//update app.js
const EventObject = {event: "",isError: false, Error: null} 
document.addEventListener("DOMContentLoaded", ()=> {
   /**
    * @type {HTMLButtonElement}
    */
   const btn = document.querySelector("#btn")
   container = document.querySelector(".container") 
   btn.onclick = () =>   worker.postMessage({...EventObject, ...{event: "pokemonFetch"}})
    // will be hoisted
    const worker = new Worker(new URL("./thread.js", import.meta.url))

})
Enter fullscreen mode Exit fullscreen mode

We are sending pokemonFetch event to the worker, where fetching will be handled, and the worker will respond with the pokemonFetch_Res event, let's handle that below:

//update app.js
document.addEventListener("DOMContentLoaded", ()=> {
...

    worker.onmessage = e => {
       console.log(e)
        const {data: {event, isError, Error, res}} = e  

        if(!isError){
            switch(event){
              case "pokemonFetch_Res":
                  console.log(res, "response")
                  AddToDom(res.results)

              default:
                  break;
            }
          }else{
              console.error(Error)
          }
    }

})
Enter fullscreen mode Exit fullscreen mode

Everything we are doing in the switch we have discussed above, we only need the AddToDom function,

which will take the results, given there's no error, and create dom elements from them:

// app.js
const EventObject = {event: "",isError: false, Error: null} 

let container; 
/**
 * 
 * @param  {Array<{url: string, name: string, img: string}>} results 
 */
function AddToDom(results){
    results.forEach(v => {
        const card = document.createElement("div")
        card.classList.add("card")
        const img = document.createElement("img")
        img.src = v.img
        card.appendChild(img)
        const label = document.createElement("label")
        label.innerText = v.name
        card.appendChild(label)
        if(container)
           container.appendChild(card)
    })
}


document.addEventListener("DOMContentLoaded", ()=> {
...
})

Enter fullscreen mode Exit fullscreen mode

The code is creating cards, that looks like image below:

pokemon cards

Here is the css for the entire thing, you can place it in separate file, for me I placed it in the head element, inside a style tag in index.html:


<style>
        * {
            box-sizing: border-box;
        }

        body{
            margin: 0;
            padding: 0;
            width: 100vw;
            height: 100vh;

        }

        .app{
            width: 100%;
            height: auto;
            background: whitesmoke;
            /* display: flex;
            flex-direction: column;
            gap: .5em; */
            padding: 5em;
        }

        .container{
            display: grid;

            grid-template-columns: repeat(4, 1fr);
         }

        .card {
            display: flex;
            /* flex: 1; */
            align-items: center;
            justify-content: center;
            flex-direction: column;
            width: 200px;
            height: 200px;
            box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);

        }

        .card img {
            width: 50%;
            height: 50%;
            object-fit: contain;

        }

        #btn{

                    display: inline-block;
                    outline: 0;
                    cursor: pointer;
                    padding: 5px 16px;
                    font-size: 14px;
                    font-weight: 500;
                    line-height: 20px;
                    vertical-align: middle;
                    border: 1px solid;
                    border-radius: 6px;
                    color: #24292e;
                    background-color: #fafbfc;
                    border-color: #1b1f2326;
                    box-shadow: rgba(27, 31, 35, 0.04) 0px 1px 0px 0px, rgba(255, 255, 255, 0.25) 0px 1px 0px 0px inset;
                    transition: 0.2s cubic-bezier(0.3, 0, 0.5, 1);
                    transition-property: color, background-color, border-color;





        }

        #btn:hover {
                        background-color: #f3f4f6;
                        border-color: #1b1f2326;
                        transition-duration: 0.1s;
                    }


    </style>
Enter fullscreen mode Exit fullscreen mode

Everything for the main thread is complete, let's navigate to threads.js, and finish the entire cycle:

// thread.js 
const EventObject = {event: "",isError: false, Error: null} 


self.onmessage = e => {
    const {data: {event, isError, Error}} = e 
    if(!isError){
      switch(event){
        case "pokemonFetch":
             pokemonFetch().then(async (v) => {
               // implemented below
                // checking if it's an error (pokemonFetch() returns type Err or Response)
                if(v instanceof Response){
                     const res = await v.json()
                     // implemented below
                      constructImagesUrl(res)
                     self.postMessage({...EventObject, ...{event: "pokemonFetch_Res", res}})
                }else{
                    self.postMessage({...EventObject, ...{isError: true, Error: v}})
                }
             }).catch(err=> {
                self.postMessage({...EventObject, ...{isError: true, Error: v}})
             })
        default:
            break;
      }
    }else{
        console.error(Error)
    }

}
Enter fullscreen mode Exit fullscreen mode

Threads.js is a little involved, the code is still similar somewhat to the main thread code, in terms of the switch statement,

when we receive the pokemonFetch event, we make a request to the PokeApi:

async function pokemonFetch(){

    const pokemons = await fetch("https://pokeapi.co/api/v2/pokemon/")

    if(pokemons)
        return pokemons
    else 
       return new Error("failed to fetch pokemons")
}

Enter fullscreen mode Exit fullscreen mode

A normal fetch request, the Poki API only returns pokemon information, we need to construct the url for image's ourselves, they are hosted on git,

Which is what the function below is doing:

/**
 * 
 * @param {Object} res 
 * @param {Array<{url: string, name: string}>} res.results
 *
 */
function constructImagesUrl(res){
   res.results.forEach(v => {
      const temp =  v.url.split("/")
      temp.pop()
      if(temp){
          v.img = `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${temp.pop()}.png`
      }
   })
}

Enter fullscreen mode Exit fullscreen mode

And that completes our switch statement, and we finally made it, you can test the app by hitting the fetch button, the container should be populated with 20 pokemon's, to top it all we have a non-blocking main thread only focused on the UI,

We have accomplished our goal, aid the main thread!

This article is a start of many involving web threads.

Thanks for reading, please let me know your thoughts and any ideas or questions in the comments. Oh and don't forget to give this article a ❤ and a 🦄, it really does help and is appreciated!

You can connect with me on twitter, I am new!

and if you like concise content, I will be posting fuller articles and projects on ko-fi for free, as they do not make good blogging content,

Articles on Machine Learning, Desktop development, Backend, Tools etc for the web with JavaScript, Golang and Python, if that's your thing make sure to follow on ko-fi,

Or want to support the blog, which Is much appreciated:

Buy Me a Coffee at ko-fi.com

Top comments (7)

Collapse
 
sfundomhlungu profile image
sk

Please note: we can run any JS code inside the worker thread, with only few exceptions, we cannot access the DOM or use some default methods, i forgot to include this somewhere in the article, you can consult MDN for more information.

Collapse
 
doseofted profile image
Ted Klingenberg

I wish this article was around when I was first trying wrap my head around Web Workers. The reducer-like pattern is a good idea for passing data back and forth to the worker. It gets complicated when you have a bunch of functions though.

Web Workers become super powerful once used with tools like Comlink or Prim+RPC (example here). But it depends on how much you rely on workers (otherwise this approach, relying on Worker's API alone, is less code). Good read!

Collapse
 
sfundomhlungu profile image
sk

Ooh a 1000% the more functions and might I add threads, the hell it is to manage.

Another vanilla solution to the problem, albeit complex is a full blow "singleton" event driven architecture,

I've tried comlink in a project before, loved it, but never heard of Prim+RPC, but will try it immediatley always looking for more cool stuff thanks and thank you for your kinds words and reading!

Collapse
 
artydev profile image
artydev

Thank you,
Very interesting

Collapse
 
sfundomhlungu profile image
sk

My pleasure🙌, Thank you for reading!

Collapse
 
eissorcercode99 profile image
The EisSorcer

So if I understand correctly the term threads are a string of operations being carried out by a computing system, and in this example you've highlighted how an API can be fetched without Async syntax or a Promise. Because it is being run in an environment that looks within the event stack and will display at its thread completion time. The only prospects of it not running in realtime is an error pertaining to the request itself?

Collapse
 
sfundomhlungu profile image
sk • Edited

I am not sure if understand your question correctly, yes!

When you launch a browser, the main threads handles everything, painting the window,
running JavaScript etc

When you spawn a new thread, think of it as a 'copy' of the main thread but w/o the DOM functionality, it's a seperate process, and code running in that thread cannot intefere with the code in the main thread

hence we need the communication pattern:


---------------------main thread-----------------------------------------------------------

--------------------worker thread----------------------------------------------------------  

--------------------time/ Frames per second----------------------------------------------

Enter fullscreen mode Exit fullscreen mode

The main and the worker are running at the same time, what is happening in main does concern the worker unless it is communicated, vice verca

To see this in action, we all know that a:

   while(true){

  }

Enter fullscreen mode Exit fullscreen mode

loop crashes the browser, because it blocks the event loop from processing anything,

however if you move the while(true) to a worker, the browser will continue to work because the main thread is not running that loop, but the worker

So the request to PokeApi, will not in any way affect the main thread, unless it is communicate vai postMessage

NOTE: I advise against putting a while true in any JS thread, beacuse the browser already has an event loop, which will be blocked, and while true's are resource intensive