Understanding the browser’s Event Loop for building high-performance web applications. Part 1.
The most common interview question and the essential base for writing an efficient code.
“How does the Browser Event Loop work?” This question can be asked in almost every JavaScript interview. Interviewers are often scolded for bad questions, but hardly anyone would argue that this question is just great! A good knowledge of the event loop gives you an understanding of how the browser will handle the code and how not to put a spoke in the wheel, but to help it be efficient so that users of your applications get the best experience.
An event loop is similar to a project manager. It coordinates events, rendering, network requests, user interactions, and all your asynchronous code.
Below are a few challenges from our telegram channel with JavaScript quizzes. Interviewers love to ask questions like this. Check yourself. Don’t worry if you don’t understand how this code works, we’ll cover it further in the article.
Quiz 1.
This is the simplest example that demonstrates how the event loop works. By the way, this is also the most common interview challenge. Therefore, you could already meet this quiz in our previous blog post on the most difficult interview questions:
Quiz 2.
This quiz is a little more confusing.
Quiz 3.
This quiz at first glance is similar to the previous ones. However, only 17% of the audience of our telegram channel (which is incredibly strong in JavaScript, judging by the answers to other quizzes) managed to answer correctly.
To understand the code in these examples, let’s take a deeper look at the event loop.
If you answered all questions correctly (and more importantly, you can explain the result) — congratulations! Feel free to skip the “How the Event Loop works” part and go to the “Rendering” section. We will definitely surprise you there.
How the Event Loop works.
The event loop operates on a queue of tasks (or macro tasks) and microtasks.
A task (and a microtask) is a piece of work that the browser needs to do. In general, we can say that a task is a function.
Tasks examples:
- Script execution
- Parsing html
- SetTimeout callback
- Callback on mouse click or any other event
Microtasks are tasks that need to be performed immediately after the script is executed.
Microtasks examples:
- Promise callback
- Mutation Observer API
We will look at the algorithm of the event loop, helping ourselves with a graphical example for clarity.
Consider the following tasks waiting to be executed:
To simplify, the event loop goes through the following steps indefinitely:
1. If there is at least one task in the task queue, get the first (oldest) task and execute it. This is the bar()
function in our case.
2. If there are tasks in the microtask queue, execute all tasks, starting with the oldest one. The qqz()
function , then the baz()
function.
3. Update the rendering.
As a result of repeating the steps, both queues will become empty.
Call stack.
What happens to a task when it is picked up for execution? It is pushed onto the call stack. Let’s look at an example.
The first task that the browser faces is the execution of the script.
The task that is taken by the browser for execution gets into the call stack.
All functions that this code calls also get into the call stack.
If a new task is created as a result of the function execution, it is placed in the task queue.
When a function is executed, it is popped from the stack. (setTimeout
in our case)
In the “stack” data structure — the last in, the last one out. In the “queue” data structure, the opposite is true — first in, first out.
If a new microtask is created as a result of the function execution, it is placed in the microtask queue.
Next, Promise()
, bar()
, foo()
, and script
are popped off the stack in turn.
When the stack becomes empty, the task is completed. After that, it is the turn to execute all the microtasks that have accumulated during the execution of the task.
When the stack is empty and no microtasks are left in the queue, the browser can move on to the next task.
When both queues are empty, the browser does nothing, waiting for new tasks.
Now we can go back to our quizzes and see what steps the browser will go through when executing them.
Quiz 1. Explanation.
Browser steps:
- The first task to execute is the script. The result of this step:
- The
setTimeout
callback is added to the task queue. - The
promise
executor functionres => { console.log(3); res(); }
is executed synchronously. So, the output of this step:2, 3, 5
. - The
promise
callback is added to the microtask queue.
2. Execute tasks from the microtask queue. Output: 4
.
3. Execute the last task — setTimeout
callback. Output: 1
.
Result: 2, 3, 5, 4, 1
.
Quiz 2. Explanation.
Browser steps:
- Execute the script. The result of this step:
- Both
setTimeout
callbacks are added to the task queue in the order in which they occur in the code. - The callback of the second
promise
is added to the microtask queue.
2. Execute tasks from the microtask queue. Output: promise 2
.
3. Execute the oldest task from the macrotask queue. The result of this step:
- Output:
timeout 1
. - The new
promise
callback is added to the microtask queue.
4. Execute tasks from the microtask queue. Output: promise 1
.
5. Execute the last macrotask. Output: timeout 2
.
Result: promise 2, timeout 1, promise 1, timeout 2
.
Quiz 3. Explanation.
Browser steps:
- Execute the script. The result of this step:
timeout 1
andtimeout 2
callbacks are added to the task queue.- The
promise
executor functionresolve => setTimeout(resolve)
is executed synchronously. So one moresetTimeout
is added to the task queue. Promise.resolve()
resolves the promise immediately, its callback is added to the microtask queue. Because the firstpromise
has not yet been fulfilled, its callback has not yet been added to the microtask queue.
2. Execute tasks from the microtask queue. Output: promise 2
.
3. Execute a task from the task queue, starting from the oldest task. Output: timeout 1
.
4. Because the microtask queue is empty, execute the oldest task in the task queue again. As a result, () => console.log(‘promise 1’)
function is added to the microtask queue.
5. Execute tasks from the microtask queue. Output: promise 1
.
6. Execute a task from the task queue. Finally, we will see timeout 2
in the console.
Result: promise 2, timeout 1, promise 1, timeout 2
.
Rendering.
Let’s take a closer look at the rendering step.
Quiz 1.
Try to guess what will be displayed on the screen:
According to MDN:
The window.requestAnimationFrame()
method tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation right before the next repaint.
In other words, the requestAnimationFrame()
method is called right before the rendering step.
According to the algorithm, after the execution of the task (and all microtasks), the browser updates the rendering.
So, the first rendering is expected after the script execution. And then the order of displaying the letters on the screen is as follows: B, A, C
. D
will never be drawn to the screen because it shares one rendering step with the requestAnimationFrame
callback, which will be executed last.
Thus, the user would see C
on the screen. This is how this code works in Safari now.
However, browsers may skip the rendering step if they think it will not contain visual changes, or if they want to combine multiple tasks for optimization purpose. For example, a browser might decide to merge multiple timer callbacks together and skip rendering in between.
For this reason, most modern browsers executing this example will skip rendering after the script is executed, and bundle setTimeout
callbacks. Thus, there will be only one rendering for three tasks. This small omission will dramatically change the final result. The only thing the browser will render is B
.
To verify this, run the code, open chrome Dev Tools
-> Performance
tab -> click Record
, refresh the page. You will only see one frame that has been drawn.
Such an inconsistent result can be not only between browsers, but also in the same browser from launch to launch. Therefore, such ambiguous code should be avoided.
Quiz 2.
In the code below, there are two buttons that animate the logo rotation 360 degrees. One button animates with setTimeout
, the other with requestAnimationFrame
. What animation do you think will rotate the logo 360 degrees faster?
This is another example of a browser skipping a rendering step. The requestAnimationFrame()
function is called every time before rendering, so it will be called exactly 360 times. In the setTimeout()
case, the browser performs one rendering for multiple (3–4) setTimeout
calls. Therefore, such an animation will run faster, and many frames will be skipped. See demo.
Tip! Use the requestAnimationFrame()
method for animations to avoid dropping frames.
Rendering process is a very interesting topic that is beyond the scope of this article. We will definitely write about it later. Subscribe so you don’t miss out.
In conclusion.
In this blog post, we took a bird’s eye view of the event loop algorithm. In Part 2, we looked at how to use loop event functions to write efficient code:
As always, we want to encourage you to keep learning the language you write every day and let’s make IT better!
Subscribe to the telegram-channel to learn:
- how modern JavaScript works
- what optimizations are made by browsers and how to use it
- how to reduce memory consumption and improve application performance on mobile devices
- how to avoid memory leaks
- how to help the browser render efficiently.
And most importantly, all this with the help of quizzes, which make the assimilation of the material easy.
Follow us on Medium to not miss new stuff.
Pop into our LinkedIn for other cool things we do.