Hello everyone! I'd like to understand JavaScript's event loop mechanism better. How does it schedule macro-tasks and micro-tasks in a single-threaded environment? What are the interaction details between the execution stack, task queue, and rendering phase? Could you explain its workflow with examples and discuss common pitfalls in real-world development?
How does the Event Loop actually work in JavaScript?
👁️ 136 views💬 3 replies❤️ 0 likes
3 Replies
I'm curious—after all micro-tasks are executed, does the rendering phase start immediately? And if there are multiple micro-tasks, how is their strict execution order guaranteed?
In JavaScript's event loop, the execution order can be simplified into the following steps: first, synchronous code in the call stack (execution stack) is executed, then it moves to the **microtask queue**, where all ready microtasks (such as Promise's then/catch, MutationObserver, queueMicrotask) are executed in sequence. Only after all microtasks are cleared will a macrotask (such as setTimeout, setInterval, I/O callbacks, UI events) be taken from the **macrotask queue** and pushed into the call stack to continue execution. The rendering phase (repaint/reflow) is triggered after microtasks finish and just before the next macrotask begins, which is why changing the DOM inside microtasks often doesn’t show immediate rendering.
```js
console.log('script start');
setTimeout(() => console.log('macro task'), 0);
Promise.resolve()
.then(() => console.log('micro task 1'))
.then(() => console.log('micro task 2'));
console.log('script end');
```
The execution order is: `script start → script end → micro task 1 → micro task 2 → macro task`. From my frontend project experience, the most common pitfalls are mistakenly thinking `setTimeout(..., 0)` executes immediately—it’s actually pushed to the next macrotask cycle, while all synchronous code and microtasks run first. Another issue is forgetting to catch exceptions in `async/await`, where exceptions turn into rejected microtasks, causing subsequent code to unexpectedly terminate early. Additionally, frequently performing heavy computations in microtasks can block rendering, leading to laggy pages. In practice, I usually offload time-consuming logic to macrotasks or Web Workers, using microtasks only for short operations that require strict ordering. This keeps the UI smooth while avoiding unexpected execution order errors.
I'm curious—when a macrotask schedules a Promise that resolves immediately, does the browser render after the microtask queue is drained before the next macrotask, or does it wait until the original macrotask finishes?