Logo
Create

Understanding the JavaScript Event Loop with the help of the Chrome Profiler

Last Updated on18th May, 2025
feature image alt text

Profiling is commonly associated with performance optimization, but its applications extend far beyond that. For example, I had previously written on how profiling data can help estimate latency impact of infrastructure downsizing.

Increasingly, one of the things I’m starting to appreciate more about profiling is that it can help you understand how a language works.

As the title suggests, let’s look at Javascript. I have this block of code in a simple React app.

function App() {
  setTimeout(planVacation, 0);
  setTimeout(requestPTO, 0);

  checkPTOBalance();
  makePlans();

  return (
    ...
  );
}

Here, we put 

planVacation

 and 

requestPTO

 to be within 

setTimeout

 , to be executed after a 0ms delay. Note: There’s no guarantee the timer will run exactly on schedule, so the actual delay will be ≥ 0ms.

How do you think the Flame Chart for this code will look like?

First, a little background on profiling. The Chrome DevTools profiler is a wall time profiler. A wall time profiler samples the call stack at a set interval, e.g. every 1ms or 10ms.

If you have some code that looks like this:

const apple = () => {
  banana();
  beer();
}

const banana = () => {
  carrot();
}

const beer = () => {
  carrot();
}

The Flame Chart would look like

Image Description

Now, let’s go back to our slightly more complicated example.

function App() {
  setTimeout(planVacation, 0);
  setTimeout(requestPTO, 0);

  checkPTOBalance();
  makePlans();

  return (
    ...
  );
}

Here, the main 

App

 calls 

checkPTOBalance

 and 

makePlans

, and it puts 

planVacation

 and 

requestPTO

 in setTimeouts to be executed after a ≥ 0ms delays.

If you’re not familiar with Javascript, you might think the Flame Chart would look like:

Image Description

If you are familiar with Javascript, maybe you think it’d look like:

Image Description

Javascript is asynchronous and it’s common knowledge that 

setTimeout

 registers its callback to be executed later, after the specified delay.

However, actually, the Flame Chart would look more like this:

Image Description

The timeout callbacks appear separately from 

App

, even though they’re registered within 

App

.

Here are actual screenshots from Chrome DevTools:

Image Description

Followed by:

Image Description

Note: setTimeout may not always appear as a frame. In the screenshot below, it does appear. This is because setTimeout’s execution time is short, so the probability of hitting it while sampling is low.

Image Description

What do these observations say about Javascript?

If you’ve worked with Javascript, you’re probably familiar with the phrase:

“Javascript uses an event-loop to handle asynchronous executions.”

Profiling Javascript code reveals precisely how this works and what this means.

Asynchronous

As mentioned already, asynchronous means the execution of some functions can be delayed. 

setTimeout

 is a way of delaying the execution of its callback. Hence, 

planVacation

 and 

requestPTO

, the callbacks to 

setTimeout

, show up after 

checkPTOBalance

 and 

makePlans

 in the Flame Chart.

Event loop

If the callbacks to setTimeouts can be delayed, it means they’re not immediately put onto the call stack. Thus, they must go somewhere else. This somewhere else is called the task queue.

Image Description

Here’s our example again:

function App() {
  setTimeout(planVacation, 0);
  setTimeout(requestPTO, 0);

  checkPTOBalance();
  makePlans();

  return (
    ...
  );
}

Notice how 

planVacation

 and 

requestPTO

 (callbacks to 

setTimeout

) don’t appear below 

App

 in the Flame Chart even though they’re registered there and the timer is set with a 0ms delay.

This is due to how the event loop works.

An event loop is, as its name suggests, a loop. Very simplified, it’s like:

while (true) { // loop
  const nextTask = taskQueue.pop()
  nextTask.run()
}

The event loop dequeues a task from the task queue and runs it. When the task is running, the control of the program is given to the call stack. When the task itself returns, the control of the program is given back to the loop. The loop will then dequeue the next task and invoke it, and so on and so forth.*

In our example, we first push 

App

 into the call stack. 

App

 calls 

setTimeout

 and 

setTimeout

 gets added to call stack. It schedules 

planVacation

 to be added to the task queue after the specified delay and then returns and leaves the call stack. Repeat for the second timeout. 

App

 then calls 

checkPTOBalance

, and 

checkPTOBalance

 will get pushed onto the call stack. After 

checkPTOBalance

 returns, 

makePlans

 will get pushed onto the call stack.

When 

App

 eventually returns and is removed from the call stack, the control of the program is given to the event loop, and the event loop will dequeue 

planVacation

 and run it.

Image Description

Thus, 

planVacation

 and 

requestPTO

 will never be part of the same call stack as 

App

 because the event loop will only dequeue these tasks when the existing call stack is empty.

Event loop with Promise

Let’s make our example slightly more complex and add in a Promise.

function App() {
  setTimeout(requestPTO, 0);

  prepareBudget().then(planVacation);

  checkPTOBalance();
  makePlans();

  return (
    ...
  );
}

const prepareBudget = () => {
  return new Promise((resolve) => {
    ...
  });
}

Will 

planVacation

 be executed before or after 

requestPTO

? Maybe you’d think after, since the timeout has 0ms delay, and 

planVacation

 has to wait for 

prepareBudget

 to resolve.

In actuality though, we see:

Image Description

Followed by:

Image Description

The Promise callback 

planVacation

 gets executed before the timeout callback 

requestPTO

. That’s unexpected?

If Promises are put into the same queue as the timeout callbacks, and if queues operate on a first-in-first-out principle, then we’d see the Promise execute after the timeout callbacks. This means Promises have to be put into a separate queue than the timeout callbacks.

In Javascript, Promises are put into the microtask queue. You might have noticed from the screenshot above the “Run Microtasks” frame.

Image Description

Both the call stack and the microtask queue are part of the Javascript V8 engine. The event loop and the task queue are not. Tasks in the microtask queue will get executed before the control of the program is passed back to the event loop.

Image Description

Once 

planVacation

 is done and the microtask queue is empty, the event loop gains control of the program back and will push 

requestPTO

 onto the call stack.

Profiling has many use cases other than performance optimization. Cost saving is one of them. As illustrated in this article, the Chrome Profiler reveals many insights into the internal workings of Javascript. Thus, understanding how a language works under the hood is another use case of profilin

Understanding the JavaScript Event Loop with the help of the Chrome Profiler