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.
src\
app.js
thread.js
index.html
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>
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))
})
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.......")
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()
})
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")
})
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")
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")
})
In the worker :
console.log("Thread running.......")
self.onmessage = e => {
console.log(e)
}
self.postMessage("hello from worker thread")
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}
When we post a message, we override or add properties to this object, as required for our use case,
worker.postMessage({...EventObject, ...{event: "pokemonFetch"}})
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)
}
}
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))
})
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)
}
}
})
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", ()=> {
...
})
The code is creating cards, that looks like image below:
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>
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)
}
}
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")
}
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`
}
})
}
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:
Top comments (7)
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.
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!
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!
Thank you,
Very interesting
My pleasure🙌, Thank you for reading!
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?
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:
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:
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 workerSo 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