JavaScript is the single most controversial language for its single-threaded nature and how biased it is on how to handle different tasks assigned to it for execution. In my opinion, the language must not be blamed for it. It could be the requirement of the context in which JavaScript is supposed to run. Yes, it’s runtime.
We all know the famous event loop. In this article, I’ll try to explain in the simplest way; how event loop handles different tasks assigned to it.
In the browser environment, JavaScript despite being a single-threaded language is given multiple tasks (synchronous and asynchronous) to perform. These tasks include rendering the screen including calculating the layouts, applying CSS, and other necessary stuff for efficient rendering of the elements on the screen. It also gives the tasks handling different DOM events like button clicks etc. On top of that, it also needs to execute the code you and I as dev write to make the page interactive and build useable applications. This code could be fetching remote data that the runtime does not even know the execution time of. The poor language has to handle everything in parallel while maintaining a smooth user experience. The language handles all this by leveraging mainly two things:
- Task Prioritization (via callback queues)
- Web APIs
In the case of NodeJS, WebAPIs are replaced by C++ APIs and the same goes for other newer runtimes.
The main focus of this article is to elaborate Task Prioritization and how event loop handles different task queues. There are mainly following queues:
- Micro-Tasks queue
- Macro-Tasks queue
- Animation callback queue
Micro-Tasks
Microtasks are tasks with higher priority, typically involving promises
(Promise.resolve
), process.nextTick
(Node specific), and MutationObserver
. When a microtask is added to the queue during the execution phase, it will be executed before the next rendering, I/O, or timer task. Since the micro-tasks are of the highest priority, the event loop will start processing micro-tasks as soon as callstack is available empty and keep processing tasks/callbacks in micro-tasks queue until there are none left.
Here is a simple flow of event loop processing micro-tasks as soon as callstack is empty.
In other words, once the micro-tasks queue claims execution, the event loop will keep processing tasks in the queue as long as there are tasks available before moving ahead. Thus, micro-tasks are the event loop’s highest priority.
Macro-Tasks
Macro-tasks include tasks like setTimeout
, setInterval
, I/O operations, and UI rendering. These tasks have a lower priority than microtasks. When the call stack is empty, the event loop picks up the next macro-task from the queue and executes it. The primary difference is that not all tasks are picked up for execution once the execution context is given to the macro-tasks queue. Only the first one in the queue is picked up and executed. And event loop moves back to handling other potentially more important tasks like micro-tasks. Here is a simple flow demonstration.
Animation callbacks
The animation callback queue in browser environments plays a pivotal role in orchestrating smooth and synchronized animations on web pages. Specifically tied to the requestAnimationFrame()
method, this queue handles tasks related to rendering updates and animations. When requestAnimationFrame()
is utilized to schedule animation tasks, these callbacks are placed in the animation callback queue. These tasks are then executed before the subsequent repaint of the browser's display (as per web standards).
The number of tasks going to be processed in one iteration of the event loop depends on the number of tasks available in the animation callback queue at the time of starting the execution of animation callbacks. Any task added to the animation callbacks queue will be processed in the next iteration. Here is a simple demonstration.
So, there’s not a single callback queue where the callbacks are queues to be executed by the event loop but a group of queues with different priorities. Event loop process callbacks based on the queue’s priority. Thus, the event loop is biased towards some tasks that are of higher importance than others.