A picture of a looping on a roller coaster ride with a big sign reading Turbo

A Quick guide to the JavaScript Event Loop (Browser)

I finally found some time to write a new article. Lately I had a talk about IT and I realized that while I know most of the topics and how they work, I have really hard times explaining them. So I looked online on how others do it and only found very huge and deep articles on those topics. So, what I want to achieve is to create very brief explanation that give you a quick and good overview about complex topics. These articles are perfect to bookmark and review in case you need to refresh your knowledge. I’ll also add resources in case you want to dig deeper. Today we will have a look at the JavaScript event loop in the browser. Enjoy reading!

TOC

  1. How does the event loop look like?
  2. What is a Stack?
  3. What is a Queue?
  4. 1. The Call Stack
  5. 2. The Heap
  6. 2b. Web API
  7. 3. The Message Queue
  8. (non) Blocking
  9. What comes first (example)
  10. Best Practice
  11. Further Reading

How does the event loop look like?

Here is my representation of the event loop:

The JavaScript Browser Event Loop

As you can see there is a loop list, a heap which is just an allocation of memory somewhere there also resides the Web API that will handle our asynchronous activities. Then we have a stack where the event loop will place and handle our synchronous calls and a message queue where the asynchronous callback handlers will be placed.

Let me explain some concepts here quickly:

What is a Stack?

A stack is a conceptual structure in which elements can be placed. The concept of a stack is FILO which means that the first element which you place in a stack is the last element that will get out of the stack.
A real world example of a Stack would be a PEZ Dispenser (if you don’t know what that is, I found a PEZ video tutorial online).

Star Wars PEZ dispenser

What is a Queue?

I think that you are all aware about what a queue is. It is a conceptual structure following the FIFO concept, which means that the first element which you place in a queue is the first element that comes out.
There are plenty real world examples of queues. For example when you do your groceries, the line in which you stay for the checkout is a queue.

People waiting in line at a grocery store

1. The Call Stack

You JavaScript code will be parsed one after another and added to the Stack. Let’s suppose you have following code:

function foo() {
  return 1 + 1;
}

function bar() {
  console.log(1);
  const second = foo();
  console.log(second);
}

bar();

Considering our representation above, this is what would happen when executing the code:

  • Start code:
    [main()]
  • Push bar() to the stack:
    [bar(), main()]
  • Push console.log(1) to the stack:
    [log(1), bar(), main()]
  • Pop (resolve) console.log(1) from the stack:
    [bar(), main()] => log 1
  • Push foo() to the stack:
    [foo(), bar(), main()]
  • Pop (resolve) foo() from the stack:
    [bar(), main()] => return 2
  • Push console.log(second) to the stack:
    [log(2), bar(), main()]
  • Pop (resolve) console.log(2) from the stack:
    [bar(), main()] => log 2
  • Pop (resolve) bar() from the stack:
    [main()]
  • Pop (resolve) main() from the stack:
    []
  • Exit the stack

Here is an excerpt from a talk on the JS Conf which explains it quite well:

Stack Example JSConf

2. The heap

The heap is just an unstructured region of memory that is used for execution.

2b. Web API

Are handling the asynchronous operations, they are responsible to hold timeouts, promises etc. until they get resolved and push the callback function to the message queue. More on that below:

3. The Message Queue

When we use an asynchronous method, the web-api will take care of its delay time and place it into the queue as soon as it is resolved.
When the Call Stack is empty, the message queue is resolved.

Let’s consider this example:

console.log(1);

setTimeout(function callback() {
  console.log(3);
}, 5000);

console.log(2);

For comparison, I will represent the Web API as brackets () and the queue like this <>. This is by no means how a queue or API looks like.

  • Start code:
    [main()] / () / <>
  • Push console.log(1) to the stack:
    [log(1), main()] / () / <>
  • Pop (resolve) console.log(1) from the stack:
    [main()] => log 1 / () / <>
  • Give setTimeout to the Web API:
    [main()] / (setTimeout[5s]) / <>
  • Push console.log(2) to the stack:
    [log(2), main()] / (setTimeout[5s]) / <>
  • Pop (resolve) console.log(2) from the stack:
    [main()] => log 2 / (setTimeout[5s]) / <>
  • Pop (resolve) main from the stack:
    [] / () / <>
  • Exit the stack
  • The Web API timeout is over. It enqueues the callback function to the Message Queue:
    [] / () / <callback()>
  • As the Stack is empty, the message queue is processes and dequeues callback by pushing it to the Stack:
    [callback()] / () / <>
  • Push console.log(3) to the stack:
    [log(3), callback()] / () / <>
  • Pop (resolve) console.log(3) from the stack:
    [callback()] => log 3 / () / <>
  • Pop (resolve) callback from the stack:
    [] / () / <>
  • Exit the stack
  • The queue is empty:
    [] / () / <>
  • Exit the queue

Here is an excerpt from a talk on the JS Conf which explains it quite well:

Queue Example JSConf

(non) Blocking

As mentioned before that refers to JavaScript handling events sequentially. The call stack can not be interrupted while running. The Queue is only processes when the call stack is empty. The timer on the setTimeout is just the minimum time to wait, it can take longer to be executed since it has to wait until the call stack is empty and all entries that are in the queue before the timeout de-queued.
So the calls executed on the web-api are referred as non-blocking. Anything executed on the call stack is considered blocking as it prevents everything else from being executed. For example if you have a long-running function on the call stack, it will even prevent user interaction like even clicking on buttons on the page. An infinite loop for example will block the execution of anything else forever and thus crash the browser tab.

What comes first (example)

So now having all this knowledge, what will be executed first?

console.log(1);
setImmediate(() => console.log("immediate"));
requestAnimationFrame(() => console.log("animation"));
requestIdleCallback(() => console.log("idle"));
setInterval(() => console.log("interval"));
setTimeout(() => console.log("timeout"));
Promise.resolve().then(() => console.log("promise resolved"));
console.log(2);

Ok, that was mean because I added some requestIdleCallback and setImmediate which are non-standard. The output is actually:

1
2
promise resolved
immediate
animation
interval
timeout
idle

interval interval interval etc…

Which surprised me because based on the specs immediate should come later. But it’s currently non-standard and only implemented in Internet Explorer. Apart from that, the promises are resolved immediately and handled before the timeout or interval. Timeout is usually handled last (except for requestIdleCallback which really waits until nothing else is running), so as you understand the timeout milliseconds is just a minimum wait time and is not always accurate.

Best Practice

  • Try to avoid long-running (blocking) methods by reducing the time complexity (for example avoiding unnecessary loops).
  • If the long-running method can not be avoided, delegate it to a web worker. Those will be executed in background threads and thus not block the main execution.
  • Avoid infinite loops. This might sound like a no-brainer but it’s still worth mentioning, and I think that now you understand why infinite loops are crashing the web-app.
  • Avoid ducktaping using the message queue. Often timing issues are solved by placing methods to the message queue using setTimeout. However, this will quickly lead to an unmaintainable code base because it is hard to predict what is in the message queue in what order. Rather find and fix the root cause.
  • Don’t use a timeout as a callback. For example, when you want to remove a div element after playing an animation (for example slowly putting the opacity to 0 to fade it out). You should rather use genuine callbacks like onanimationend or ontransitionend because as we learned, the setTimeout will not always be accurate in terms of time.

Further reading

I hope that this brief overview could give you a solid understanding of the JS Event Loop. If you’re feeling like you want to dig deeper, here are some articles I recommend on the topic:

Thank you for reading and feel free to ask any question or let me know if you found any mistake.

PS: don’t forget to share the knowledge and to follow me on twitter or via email if you liked this post :)

4 Great Posts

Comments