Event Loop Fundamentals
The Browser Event Loop is probably the most important frontend performance concept to grasp. Its behavior dictates how web applications behave at runtime.
The Main Thread
Before we can talk about the Event Loop, we need to introduce some key concepts. Let's start with the Main Thread.
Web applications are required to execute certain key operations on this single Main Thread. When you navigate to your web application, the browser will create and grant your application this thread for your code to execute on.
Most of your web application's runtime activity takes place on this single thread, and each activity on the thread is invoked within a Task.
Note: This does not mean the Browser (i.e. Chrome) is single threaded. Modern browsers are both multi-process and multi-threaded.
Tasks
Tasks are units of runtime execution that are executed on the Main Thread.
Some common examples of Tasks include:
- Parsing of HTML or CSS
- User Input Handling, such as clicking or typing
- Compiling and Executing JavaScript
- Receiving Network Data
- The Render Steps (Style, Layout, and Paint; discussed later)
Certain Tasks must run on the Main Thread. For example, anything that directly requires access to the DOM (i.e. document
)
must be run on the Main Thread (because the DOM is not thread-safe). This would include most UI related code.
Only one Task can run on the Main Thread at a time.
Furthermore, a Task must run to completion on the Main Thread before another Task can be run. There is no mechanism for the browser to "partially" execute a Task -- each Task is run in its entirety to completion.
In the example below, two tasks are run sequentially and each to completion on the Main Thread:
Task Queue
It is impossible to run multiple Tasks simultaneously on the Main Thread, so any Task that is not actively running resides in a data structure called the Task Queue.
As your web application executes on the Main Thread, the browser queues Tasks into this data structure.
It also reads from this queue when the Main Thread is ready to execute the next Task.
Note: The Task Queue is not a FIFO queue. Most browsers implement heuristic-based scheduling logic, ranking some Tasks above others.
The Render Steps Task and Frames
The Render Steps Task is responsible for the translation of the active DOM and CSS Styles into visual pixels on the user's screen.
What's produced after the Render Steps Task completes is known as a Frame.
Edits to the DOM or CSS Styles for a web page will notify the browser that it needs to generate a Frame. When the browser detects it needs to generate a Frame, it'll run the Render Steps Task instead of running a Task from the Task Queue:
There are generally 3 key steps for the Render Steps to complete:
- Style - Assign CSS colors, fonts, and other styling properties to visual elements
- Layout - Position visual elements with precise coordinates
- Paint - Instruct graphics libraries on how to draw positioned, styled visual elements
For more details on each step, check out my detailed tip on the browser rendering pipeline.
The Event Loop
Finally, with all of these foundational topics covered, we can finally discuss the Event Loop!
The Event Loop is the orchestrator between these foundational pieces.
It is essentially an infinite loop, responsible for:
- Selecting an available Task from the Task Queue, and placing it on the Main Thread to run to completion
- Occasionally selecting the Render Steps Task instead of reading from the Task Queue to generate a Frame
This pseudo-code snippet summarizes the Event Loop:
while (true) {
var nextTask;
// The browser notifies the Event Loop
// when it's time to render a frame
if (shouldRenderFrame()) {
nextTask = getRenderStepsTask();
} else {
// If it's not ready for a frame,
// the Event Loop should run the
// next Task.
nextTask = taskQueue.next();
}
// Run Task to completion
runOnMainThread(nextTask);
}
On each turn of the Event Loop, it decides either to select a Task or to render a Frame to the screen.
What is key to understand is that the browser can either run a Task or generate a Frame, but it cannot do both at the same time. (there are limited exceptions to this, but I will not discuss them in this tip)
Whichever ends up running, it's run until completion.
In general, Task durations should be short so the browser can invoke the Render Steps more quickly to produce a Frame to your users. This is covered in more detail on my avoiding Long Tasks tip.
Note: The Event Loop is a C++ construct and is not written in JavaScript, I just happened to use that for this pseudo-code.
Non-Blocking
The Event Loop, by design, never pauses or blocks the Main Thread; it will continuously loop and run Tasks and produce Frames.
But what happens when we need to do an asynchronous task? For example, if our web application requests network data or utilizes a timer, how does this manifest in the Event Loop?
Each asynchronous primitive provided by the browser is represented as a Task that is compatible with the Event Loop execution model.
Example asynchronous Browser APIs include:
- Loading network data via
XMLHttpRequest
orfetch
- Responding to events via
addEventListener(...)
- Timers like
setTimeout
orsetInterval
For example, utilizing setTimeout(callback, 500)
does not block the Main Thread, but tells the browser to queue a
Task to run callback
on the Task Queue after 500ms.
Example: Timer
Let's put together everything we've covered in a simple example.
Our example will be a simple script that edits the DOM in a Timer callback every 500ms.
let count = 0;
// Queue a Task on the Task Queue every 500ms
setInterval(() => {
// This code runs in a Task
count++;
const node = document.getElementById('output');
node.innerText = `${count} Timers Fired!`
}, 500);
Since this edits the DOM in a Task, the browser will need to generate a Frame to represent the changes as pixels.
Initially, after this script is evaluated, our Main Thread will be free, and the Task Queue will be empty. The browser has been directed to queue a Task after 500ms.
Though the thread has no activity happening during this 500ms waiting time, it is free and able to respond to clicking, typing, or other Tasks if there were any queued.
After 500ms, the browser will queue a Task for running our callback for editing the DOM:
The Event Loop detects a Task queued to be run, and moves it from the Queue to the Main Thread to run (since the Main Thread is not busy doing anything else).
This task is run and edits the DOM, which notifies the Event Loop to run the Render Steps:
Once this Frame is produced, our updated text appears on the screen!
Every 500ms, these exact phases will continually take place, with a Timer Tasks being queued. The Event Loop will remove each from the queue, and place it on the Main Thread to run. This will cause the Event Loop to run the Render Steps Task and produce a Frame.
You can profile this example and see the Tasks and Frame Generation in the profiler!
Here's a screenshot of it happening in the Profiler:
While this simple example is just a Timer, you can generalize this to any action in your web application, whether it's a button click or receiving network data.
Conclusion
Almost all operations that a web application does can be boiled down into Tasks queued into the Task Queue, subsequently run on the Main Thread via the Event Loop.
Tasks that require a visual update rely on the Event Loop to interleave Render Steps Tasks to produce Frames between other Tasks.
You can leverage this knowledge to improve your user experience, by optimizing how and when your Tasks execute.
Consider these tips next:
That's all for this tip! Thanks for reading! Discover more similar tips matching Browser Internals.