Event Loop, Single Thread, Async JS, Task Queue and more...

Event Loop, Single Thread, Async JS, Task Queue and more...

*Note: Explanations in this article are made in the context of the Chrome Browser.

👋Hello there! You might have heard that JavaScript can only do one thing at a time. But with all the async stuff Javascript lets us do these days - like callbacks, network requests, and setTimeout - execution of JS code is actually pretty complex. Ever wonder what enables all that asynchronous magic🔮 under the hood? There's also an event loop keeping everything in sync🕓.

In this article, I'll demystify the feats of modern JavaScript and show you what's really going on to power the asynchronous awesomeness.

Here's a simple diagram to help demystify how your browser works. Don't worry, just keep reading the article and it'll all make sense soon.

How JavaScript is executed?

In Chrome, as you know V8 engine runs the JavasScript, it is a JS runtime that uses a 📚Call Stack to keep track of what functions and scripts it should execute next. Each function or script is added ("pushed") to the top of the Call Stack. Then, the functions are executed in the order they were added, finishing one before moving on to the next ("last-in-first-out"). Under the hood, Chrome has just 🧶one main thread that the V8 uses. So the runtime takes turns running each function on this single thread.

Let's see how Async Callback works.

Check this snippet, and try to guess the output.

const doSomething = () => console.log("I made a website🧑‍💻");
const greetings = () => console.log("👋Hello, there!");
setTimeout(doSomething, 2000);
greetings();
console.log("Friend");

Its output will be:

👋Hello, there!

Friend

and after a slight delay

I made a website🧑‍💻

Here's what happened:

Let's take a casual stroll 🚶‍♂️ through how this code executes! First, the runtime starts executing the code line by line. When it reaches setTimeout(), it politely hands off execution to a separate thread in the browser. The separate thread then kindly starts a 2000ms timer for us. Meanwhile, the greetings() function is executed, printing 👋Hello, there! .Then the console.log() function is also executed, printing Friend . After 2000ms have passed, the timer finishes. Now it's time to run the doSomething() callback! The doSomething() callback is added to the task queue, which organizes all the callbacks that need to be executed. Since the queue operates on a first-come, first-served basis, the oldest callback is handled first. The Event loop then retrieves the doSomething() callback from the queue and adds it to the Call Stack. Then runtime executes doSomething() and outputs: I made a website🧑‍💻 .

What are queues?

First, I want to clarify that some of the Javascript functions we use such as setTimeout(), fetch(), promises(), etc, are actually provided by web browsers. They are not part of the core JavaScript language. The browser runs these functions in the background, separate from the main thread. They are known are 🛰️Web APIs.

When the JS runtime encounters one of these browser-provided functions, it sends the function to a separate browser thread to run in parallel. Later, the function's callback (the code that should run after the function finishes) is added to a queue.

In Chrome, callbacks are stored in queues to be run in the proper order. There are a few queues, but for now, we'll focus on two: The Task Queue and The Job Queue.

The Task Queue is where most callbacks end up, like the callback doSomething(). The event loop will grab the callback from the task queue when the Call Stack is empty and push it onto the Call Stack to execute the callback.

The Job Queue, also called the Microtask Queue, is quite different from the former. It only stores callbacks from promises and a few other APIs. The job queue has a higher priority.

Note: The callbacks in Queues are also called Tasks.

What is an Event Loop?

The event loop is a continuous process ♾️ that runs in the background of a browser. It manages the flow of tasks of events. Whenever the Call Stack is empty🗑️, the event loop checks🔍 if there are any tasks waiting in the task queue. If so, it takes the first task from the queue and pushes it onto the Call stack, where it will be executed. After a task from the task queue is executed, the event loop is not required to fetch the next task from the task queue. It can take a detour and do other work, which will be discussed later. The event loop is a diligent worker that keeps the show running.

When dealing with microtasks, the event loop will fetch the oldest microtask from the microtask queue and push it onto the Call Stack. The Call Stack will then execute the microtask. Once the microtask finishes executing, the event loop will fetch the next oldest microtask from the microtask queue and push it onto the Call Stack. This process will continue until all the microtasks in the queue have been processed. Additionally, while executing microtasks, they can spawn child microtasks which must also be executed.

Remember that microtasks have first dibs on getting done. So, if there are tasks waiting in both the regular task queue and microtask queue with nothing else going on, the event loop will tackle the microtasks before the regular tasks.

Just wanted to circle back to something I said earlier. After a task finishes up, the event loop might take a quick little side trip. What's it up to? It can either handle the microtask queue or another queue called the Rendering Queue.

There are other queues as well. One important queue is the rendering queue, which stores the tasks that need to be performed to render or refresh a webpage in the browser. The rendering tasks are executed by the browser's rendering engine, like the V8 Call Stack. It also runs on the main thread. Since the rendering queue is also important, the event loop manages it in addition to the task queues. Just like with a task queue, the event loop will take a rendering task and hand it off to the rendering engine to get the job done.

Browsers are pretty clever🧑‍🎓 when it comes to rendering webpages. They don't need to refresh the display constantly, so they only add items to the rendering queue when necessary. Since most computers can show 60 frames per second, the rendering queue is updated about once every 16 milliseconds. The browser has 16 milliseconds to clear out the task queues and rendering queue, which seems like a decent amount of time. However, if certain tasks take too long to complete, it can lead to laggy or jumpy animations and UI interactions.

For more information on the Rendering process, check this article.

I hope this glimpse into the inner workings of JavaScript and browsers has been helpful and illuminating. Thanks for coming along for the journey!

Did you find this article valuable?

Support Debarshi Raj Basumatary by becoming a sponsor. Any amount is appreciated!