
One of the major thing that we always grew up with was that JavaScript was single threaded with a global execution context. This was always a limitation and we would try to work around it so that we can have a responsive UI.
Over the years several modifications were done to the language so as to overcome this restriction from the language.
History Lesson
The first solution that was tried is to have an event loop and callback based async calls. In this scenario there are three moving pieces. There is the call stack and callback queue and the glue between them is the event loop. Call stack as the name suggests is a stack that will keep a list of all executing functions. It is LIFO, and will execute the last function that comes in first. If a function has a callback attached, it will be pushed to the callback queue for processing. After the function is done, it gets sent to Event loop. Event loop will take care of pulling functions to be executed from call stack and putting them on callback queue.
Unfortunately, as codes became complicated and callbacks started getting misused, eventually starting to have a lot of different callbacks that would be very difficult to debug. The other associated problem was that errors from so many nested callbacks was getting more challenging to manage. To eliminate this problem, a wrapper was put around the callbacks. We call this wrapper as Promise. We will instantiate a promise using Promise() constructor and it will manage all errors and responses for us.
Eventually JavaScript brought in the concept of async/ await for solving the problem of single thread program execution. This provides a much easier syntax to solve the problems of asynchronous execution. It still works on top of promises that we discussed, however from a new developer perspective, it becomes more manageable. It is also easier to understand and debug code.
Origin of Web Workers
All of the models discussed above eventually use the initial event loop model discussed. So theoretically they are not different. However, imagine a situation where we need a large number of CPU cycles to process some instruction. We will still be blocking and slowing down the main thread in this case. This is what Web Workers tend to solve. Most modern browsers in use today support web workers. Web workers will execute a task in a different thread altogether thus freeing up the main thread for other functions. Theoretically you can create as many threads as the underlying operating system supports making it a very useful solution in some cases.
About Web Workers
As mentioned, web workers are a means of running scripts in the background. They run in a separate thread freeing up the master thread for other processing. Most modern browsers support Web Workers. There are two different variations of Web Workers.
- Dedicated Workers
- Shared Workers
We can only call dedicated workers from the script that initiated it. On the other hand, we can call a Shared worker from multiple scripts. That means they can reside on different windows or scripts. This is a very convenient feature from the perspective of an application wanting to use a feature across the application.
However, web workers do come with their set of limitations.
- Web Workers cannot access or manipulate DOM directly
- Web Workers run in a different context, called WorkerGlobalScope
- Web Workers always needs a web server to run
- Web Workers can only load files from same origin
In the next section we will build a simple application and implement a long running function in a synchronous mode and then using a web worker. We will have an animation playing continuously, so that we can see how the web worker will use a separate thread.
Base Application
We will build a continuously moving bouncy ball application for the primary thread. It will be very easy to spot any interruption.
.board { height: 85vh; width: 90vw; background-image: linear-gradient(to bottom, #363532, #13100e); border-radius: 14px; } .ball { height: 60px; width: 60px; border-radius: 50%; position: fixed; top: calc(50% - 30px); left: calc(50% - 30px); background-image: linear-gradient(to right, #cfd3da, #8e9493); }
We use the above two classes for styling the ball and board. I will only provide partial code here. You can can get the full code on my GitHub link here.
I will be using the following code to have the ball continuously bounce around in the board.
let ball = document.querySelector('.ball'); let ball_pos = ball.getBoundingClientRect(); let board = document.querySelector('.board'); let board_rect = board.getBoundingClientRect(); let animated = false; function moveBall(dx, dy, dxd, dyd) { if (animated) { if (ball_pos.top <= board_rect.top) { dyd = 1; } if (ball_pos.bottom >= board_rect.bottom) { dyd = 0; } if (ball_pos.left <= board_rect.left) { dxd = 1; } if (ball_pos.right >= board_rect.right) { dxd = 0; } ball.style.top = ball_pos.top + dy * (dyd == 0 ? -1 : 1) + 'px'; ball.style.left = ball_pos.left + dx * (dxd == 0 ? -1 : 1) + 'px'; ball_pos = ball.getBoundingClientRect(); requestAnimationFrame(() => { moveBall(dx, dy, dxd, dyd); }); } }
Now that we have a continuous moving ball, next thing we concentrate on is to have a long running process.
Long Running Process
We will create a unoptimized implementation of counting how many primes are there to a given max number. We will keep this as a separate file so that we can refer the function from different scripts.
function isPrime(n) { for (let i = 2; i <= Math.sqrt(n); i++) { if (n % i === 0) { return false; } } return n > 1; } function findPrimes(maxnum) { const primes = []; for (let i = 2; i <= maxnum; i++) { if (isPrime(i)) { primes.push(i); console.log(i); } } return primes.length; }
Now those two out of the way, we will next call this function in a synchronized way.
Synchronized Function call
This one is easy. We just create a simple function within the same context to invoke the findPrimes() method.
function calc_primes(n) { return findPrimes(n); }
This we invoke from a different JavaScript file with a large number.
else if (e.key == 'b') { kmsg.innerHTML = 'b'; let v = calc_primes(10000000); alert('Number of Primes: ' + v); }
As soon as the function is invoked, we see the screen freeze while this method is being executed.

Dedicated Web Worker
Web Workers rely on messages to transfer data. In this case we will write a message receiver that will receive message from caller thread.
importScripts("prime.js"); onmessage = (e) => { console.log('Got message with: ' + e.data); let v = findPrimes(e.data); console.log('Prime count: ' + v); postMessage(v); };
One thing of interest here is the use of importScripts. Remember I have said before that Web Worker works in a separate scope. So, no functions are accessible. To use any function, we will need to import the JavaScript file that contains this function to use it. Web Workers also transfer back result using messages. In here, we are using postMessage to send back our response.
The second part of the equation is the caller.
/* Global */ const myWorker = new Worker('workerd.js'); /* Partial code below */ else if (e.key == 'w') { kmsg.innerHTML = 'w'; if (window.Worker) { myWorker.postMessage(10000000); } }
This part will submit the message and start a dedicated Web Worker. Now how do we receive the returned messages?
The third part is the final receiver which will listen for an event to complete from the Worker thread.
myWorker.onmessage = (e) => { alert('Number of Primes: ' + e.data); }
In this case we do not see any freeze. Animation keeps on running while the bad prime calculation is done in the backend.
Finally, make sure to discard the web worker if not needed.
myWorker.terminate();

Shared Web Worker
I will not have too much code for an implementation of Shared Web Worker, but I will point out some differences. We will always access a shared web worker using a port. So, if we change the program above to a shared web worker, web worker will be coded as follows.
onconnect = (e) => { const port = e.ports[0]; port.onmessage = (e) => { console.log('Got message with: ' + e.data); let v = findPrimes(e.data); console.log('Prime count: ' + v); port.postMessage(v); } };
We have to change initiation as follows.
else if (e.key == 'w') { kmsg.innerHTML = 'w'; if (window.Worker) { myWorker.port.postMessage(10000000); } }
We will change receiver as follows to include port,
myWorker.port.onmessage = (e) => { alert('Number of Primes: ' + e.data); }
So overall, there is not too much changes for changing a dedicated web worker to a shared one.
Conclusion
The inclusion of web workers in JavaScript opened up some new avenues that did not exist before. Now we can push a lot of CPU intensive tasks to use additional threads instead of tagging on the main thread. Hope you found this information useful. As always, code is on my GitHub page. Ciao for now!
thanks for the information