How Does JavaScript Even Work? Things Which 90% of JavaScript Developers Don't Know! (Part 2)

How Does JavaScript Even Work? Things Which 90% of JavaScript Developers Don't Know! (Part 2)

Render Queue, Microtask Queue, Promises, Order of Execution and more..

·

10 min read

Note: This is Part 2 of the series title "How Does JavaScript Even Work?". I would highly recommend you to read Part 1 before reading this.

How Does JavaScript Even Work? (Part 1)
How Does JavaScript Even Work? (Part 2)

So, assuming that you have read Part 1, let us discuss on render queue and microtask queue and a few other concepts which are required for you to understand these two queues and their working.

Starting with Render Queue

Render Queue

Render queue is another queue in the event loop system which basically takes care of all tasks to be done before every screen update or repaints.

We are able to see all the effects and the animations happen because of browser doing these repaints at regular intervals.

Here, as a developer it is good to know how the repaint mechanism works.

render-queue.png

The above diagram shows the javascript tasks (the tasks queued to be executed before the repaint) and the four major steps involved in every repaint.

But there is a lot more in every repaint by the browser. Here is a fantastic blog with the title " Browser Rendering Queue in-depth."

Note that browser doesn't do all the steps on every repaint, it is smart and updates things only when it has changes.

At what frequency does the browsers repaint? The major browsers generally are set to repaint around 60 times every second or 60FPS. It is 60FPS as your eyes can't perceive faster change. But it can happen at this speed only when the the main thread is idle or the call stack is empty.

So, repaint can't happen while there is some task running on the main thread. And if there are tasks queued up, the browser cleverly runs a few tasks and then goes to rendering and then quickly comes back and so on.

We have seen in the part 1 that when we execute an infinite loop, everything on the screen just freezes, this is because the main thread is blocked doing the infinite loop and render tasks never get a chance to come in.

You can see an amazing demonstration of the render queue at this visualisation website: latentflip.com/loupe

You can run the program with simulate renders ticked and you can see how it works

image.png

Hmm okay, now let us come into JavaScript and how can you queue tasks before repaint?

In Javascript, we can line up tasks in to render queue.

And we can line up these tasks using a method from the browser called requestAnimationFrame web API.

Note: RequestAnimationFrame lines up tasks before painting steps in major browsers but it differs in safari where it lines up the tasks after all the rendering steps i.e the tasks will be executed during the next render.

Render queue has got this property wherein event loop will execute all the tasks present in it at the beginning but if we add something while its execution, it will be taken care in the next paint cycle.

That was all about the render queue, now before getting into microtask queue and how does all these queues work together with event loop and call stack, let us look into a relatively new ES6 concept called Promises which is important to understand microtask queue.

Promises

We saw in the Part 1 that we pass all the time taking tasks a call backs which run on successful execution of that task. But one can very quickly run into something called as a callback hell where you have callbacks inside callbacks inside callbacks and it just goes out of your head.

To avoid this, Promises came into picture with ES6 which are objects that may produce a single immutable value some time in the future or throw an error. Promises are used to keep track of an asynchronous event as in whether it has been executed or not.

Promises basically can be in three states namely

  1. Resolved i.e the task is successfully executed.
  2. Rejected i.e something wrong happened with the task.
  3. Pending i.e it is still not settled wherein settled means that it is either resolved or rejected.

Below is the basic code which is used to create a promise.

const p = new Promise((resolve, reject)=>{
    const a = 1;
    if(!a){
        reject(new Error("This Promise is Rejected"))
    }else{
       resolve("This Promise is resolved");
   }
})

p.then(()=>{
    console.log("Successfully Resolved");
}).catch(()=>{
    console.log("Rejected");
})

//Successfully Resolved

As we can see that a promise takes two functions namely reject and resolve. On successful task execution we call resolve otherwise we call reject.

We use the .then() method on a promise and pass on a success callback which runs when the promise is resolved. We generally use .catch() method on the promise to pass on a rejection callback which runs when the promise is rejected.

We can then use .finally() to run some code after the promise is settled.

Promises solve the call back hell with something called promise chaining and we can use async/await to handle promises in asynchronous functions which are even cleaner but let us leave these for a dedicated blog on promises.

Let us now see a queue which handles these promises and there .then(), .catch() callbacks.

MicroTask Queue

Microtask queue or also known as the Job Queue is another interesting queue in event loop which basically handles promises.

The Microtask queue has higher priority than the task queue and the render queue as well i.e the tasks in the microtask queue will be executed first.

So, once the call stack gets empty, the event loop looks up to the microtask queue for tasks and if it does have any tasks, it will finish those tasks and let me tell you that event loop will keep taking up tasks from microtask queue until the queue is empty.

Another special property of this queue is that after executing every task, the event loop will go to microtask queue and check if something is there and if it is there then it will execute all of them.

And whenever repaint time comes, once the current task ends, the event loop takes up render task, finishes it and comes back quickly.

Beware that due to this behaviour, you can run into a case similar to an infinite loop. Let us see how

<button id="kill">Block Main Thread</button>
<script>
    function blockEventLoop() {
        Promise.resolve().then(blockEventLoop);
     }

     const btn = document.getElementById('kill');
     btn.addEventListener('click', blockEventLoop, false);
</script>
<input type="text" />
<button type="submit">Submit</button>

Once you click the Block Main Thread button, everything will be freezed but why? let us understand.

-> The script runs and all the event listener addition happens.
-> You click the button Block Main Thread.
-> The event listener is called and it queues blockEventLoop function.
-> Now the call stack is empty so the event loops get the promise into call stack and while executing it, another promise gets added and this keeps on happening infinitely.
-> So, the main thread never gets idle and this stops eveything else leading to a freezed screen.

Let us see another small program to get the order of execution right. (Source: JSConf)

<button id="run">Click Me</button>
<script>
      const button = document.getElementById('run');
      button.addEventListener('click', () => {
        Promise.resolve().then(() => {
          console.log('MicroTask 1');
        });
        console.log('Listener 1');
      });
      button.addEventListener('click', () => {
        Promise.resolve().then(() => {
          console.log('Microtask 2');
        });
        console.log('Listener 2');
      });


//Output
//Listener 1
//MicroTask 1
//Listener 2
//MicroTask 2
</script>

I know the output is a little counter-intuitive, so, let us see how does it work.

-> The script runs and all the event listener addition happens.
-> We click the button and the first event listener is pushed on to call stack.
-> Promise is resolved, the callback is passed to microtask queue and Listener 1 is logged.
-> The first event listener is popped off the call stack, now as the call stack is empty, so, microtasks are executed and MicroTask 1 is logged.
-> On the same line, then the listener 2 is pushed to call stack.
-> Promise is resolved, the callback is passed to microtask queue and Listener 2 is logged.
-> The second event listener is popped off and then microtasks are again executed as the call stack is empty and MicroTask 2 is logged.
-> The execution ends.

I hope you got some clarity on microtask queue and its flow of execution.

So, what all are microtasks in JS?

What are Microtasks?

  1. Promises.
  2. Mutation Observers.
  3. queueMicrotask().

These three generate microtasks in JavaScript and are handled by the microtask queue.

So, I think that was all about the microtask queue.

Processing of Different Queues

  1. Task queue: Here the tasks are executed one at a time and new ones are added. There is not rule that all the tasks here should be finished before moving.
  2. Render queue: Once the event loop comes to the render queue, it will finish everything which was existing at the beginning, and if something is added after the event loop starts here, they are deferred to the next render cycle.
  3. Microtask queue: This queue is executed until it is exhausted. So, if you run an infinite recursion of promises, you will block the main thread as shown above.

Example With All Queues

Let us see one last program which can demonstrate tasks in all three queues.

<script>
      function log(arg) {
        console.log(arg);
      }

      // Function which will add a callback to task queue using setTimeout
      function taskQueue(number, callback) {
        setTimeout(function () {
          callback(`Timeout ${number}`);
        }, 0);
      }

      // Function which will return a promise
      function microTaskQueue(number) {
        return Promise.resolve(`Promise ${number}`);
      }

      // Pushing to microtask queue
      microTaskQueue(1).then(log);
      microTaskQueue(2).then(log);

      // Pushing to task queue
      taskQueue(1, log);

      // Pushing to render queue
      requestAnimationFrame(function () {
        log('RequestAnimationFrame 1');
      });

      //Normal code to show that the script will run till completion first
      for (let i = 0; i < 100000000; i++) {}
      console.log('The end');

</script>

// Output
// The end
// Promise 1
// Promise 2
// RequestAnimationFrame 1
// Timeout 1

I know that even this output can be a bit difficult to digest so let us see what happens step by step.

-> Firstly we call microTaskQueue(1).then(log) and microTaskQueue(2).then(log) which pushed the two log functions to microtask queue.
-> Then we call taskQueue(1, log) which pushes log function to the task queue via the setTimeout web API.
-> Then we call requestAnimationFrame() which then pushes log function to render queue.
-> Then the for loop runs and The end is logged.
-> Now the call stack is empty and the event loop comes into picture.
-> First chance is given to microtask queue and the event loop executes all the tasks in microtask queue until it gets empty so Promise 1 and Promise 2 are logged.
-> Then browser goes to rendering and RequestAnimationFrame 1 is logged.
-> Then event loop comes to task queue finally and the Timeout 1 is logged.

I hope you understood the flow of execution.

So, lastly, if I had to write a small pseudo code for the event loop, it would look like this

carbon(9).png (Source: JSConf)

Conclusion

So, that was all about the working and execution of Javascript in your browser, all the queues, stacks and loops. I hope you got some value and learning out of it.

If yes, do share the blog and follow me on Sai Teja

You can follow me on twitter also @saai_tejaa

Important Note: All the implementation of queues discussed in the two parts is a general overview, there can be additions and deletions in this depending on the browser. For example, the spec only imposes the existence on one task queue but some browsers can have more queues for different tasks like Firefox planned to have a separate task queue for all the setTimeout callbacks etc.

Some Amazing Resources

  1. How Does JavaScript Even Work? (Part 1)

  2. What the heck is the event loop anyway? | Philip Roberts | JSConf EU

  3. Further Adventures of the Event Loop - Erin Zimmer - JSConf EU 2018

  4. Jake Archibald: In The Loop - JSConf.Asia

  5. Task Queue and Job Queue - Deep dive into Javascript Event Loop Model

  6. Browser Rendering Queue in-depth

  7. What are Promises?