When a callback function is called in an existing execution context, it is pushed to the message queue. They remain in the queue until the execution stack is empty which is when the event loop finally executes these tasks. Reading the code, Queueing up the tasks, and execution of the tasks is done by the event loop.
Micro-tasks and Macro-tasks
Simply speaking, micro-tasks are used to create a queue of synchronous tasks inside the message queue which is executed asynchronously with the execution stack.
How are micro and macro tasks executed?
An event loop has a task queue which is the macro-task queue. Once it starts executing the callback functions in the message queue, it takes one macro-task, executes it and goes to execute the micro-task queue. It keeps executing the micro-task queue until it is finished. Then it goes back to the macro-tasks (task) queue and executes another one.
Render is also blocked while the micro-tasks are executing to keep the UI intact for the user. So if we are stuck in the micro-task queue for too long, the page might become unresponsive.
How to divide micro and macro tasks?
It is advised to the programmers to use micro-tasks when you want to perform some functions synchronously in an asynchronous manner. Otherwise, it is best to use macro-tasks.
Few examples of macro-tasks are setTimeout, setInterval, setImmediate, I/O tasks, UI rendering. While promises are micro-tasks as they are executed as soon as their constructor is invoked.