跪拜 Guibai
← All articles
Frontend · JavaScript · Programmer

The Event Loop's Hidden Half: Rendering, Microtask Stalls, and the 16.6ms Budget

By ErpanOmer ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Real performance problems like dropped frames, layout thrashing, and frozen pages stem from how code interacts with the rendering pipeline, not from the classic microtask-versus-macrotask ordering. Knowing the full frame sequence lets a developer fix those problems directly: keep microtask chains short, use rAF for animations, and batch DOM reads and writes to avoid forced reflows.

Summary

The browser event loop is usually taught with a one-line quiz: setTimeout logs after Promise.resolve. That model covers task scheduling but ignores the rendering pipeline, which is where real performance problems live. A complete walkthrough of a 16.6ms frame lays out the sequence: one macro task, a full drain of microtasks, a render decision, requestAnimationFrame callbacks, the Style/Layout/Paint/Composite pipeline, and finally requestIdleCallback if budget remains. Each step carries concrete consequences: recursive microtasks can starve rendering and freeze the page; continuous DOM writes in one task batch into a single render; reading layout offsets forces a synchronous reflow; and requestIdleCallback runs after paint, so mutating DOM there delays visible updates by a frame.

The same walkthrough explains why requestAnimationFrame is the only reliable animation hook: it runs before style and layout, so changes from its callback appear in the same frame. setTimeout, by contrast, lands in the macro task queue and may miss the current frame entirely. The layout thrashing example shows that interleaving reads and writes forces repeated Layout passes, while batching reads then writes cuts the work by orders of magnitude. A deliberately tricky interview problem ties it together, with an output order that depends on whether the browser decides to render that loop.

The takeaway is a mental model of the event loop that matches what the browser actually does each frame, not just what prints to the console. Knowing the order of console.logs is far less valuable than knowing when the browser paints, and where your code inserts unnecessary computation into that pipeline.

Takeaways
— Each event loop iteration picks exactly one macro task; queued setTimeouts run across separate iterations, not in a single batch.
— After a macro task, the browser drains all microtasks, including any new ones they produce.
— A microtask that schedules another microtask endlessly prevents the drain from finishing, so the render step never runs and the page freezes.
— The equivalent self-recursive loop using setTimeout stays responsive because each callback is a new macro task with rendering in between.
— Not every event loop iteration triggers a render; the browser decides based on frame timing and other conditions.
— Multiple DOM writes within the same macro task are batched into a single render, so manual batching is unnecessary.
— requestAnimationFrame callbacks run before style and layout, so changes made there appear in the current frame.
— Microtasks queued inside a requestAnimationFrame callback are processed in the same step, but an rAF scheduled inside another rAF is deferred to the next frame.
— The rendering pipeline runs in four steps: Style, Layout, Paint, Composite; JavaScript affects them only indirectly.
— Reading layout properties like offsetHeight forces a synchronous reflow, and write-read-write loops trigger repeated reflows.
— Batching all reads then all writes reduces forced reflows from one per element to a single render-time layout, a difference of orders of magnitude.
— requestIdleCallback executes after rendering only if the frame budget remains, usually 1-5ms; it suits non-urgent work, not DOM changes.
— In the article's interview problem, the output order is 1, 8, 6, 4, 5, 7, 2, 3; the rAF for '7' defers to the next frame because it was registered inside a microtask.
Conclusions

The interview-quiz model of the event loop leaves out the rendering pipeline entirely, yet most real performance issues come from that pipeline, not from whether Promise callbacks beat setTimeouts.

A microtask chain that never yields can starve rendering and freeze a page; the same logic with setTimeout keeps painting alive, making the choice of scheduling primitive a genuine performance decision.

Separating DOM reads from writes is a broad, reusable performance rule that falls directly out of how forced reflows work; it fixes layout thrashing without micro-optimization.

Concepts & terms
Macro task
A queued unit of work processed one at a time per event loop iteration, such as setTimeout, setInterval, user input events, and network callbacks. After each macro task, microtasks are drained and rendering may occur.
Microtask
A smaller unit of work queued by Promises, queueMicrotask, or MutationObserver. The browser drains the entire microtask queue after each macro task before considering rendering, which lets a self-perpetuating microtask chain block the frame.
Forced reflow
A synchronous layout calculation triggered when code reads a DOM geometry property like offsetHeight after modifying styles. It forces the browser to compute layout immediately, breaking the normal batching of writes and leading to repeated layout passes.
requestIdleCallback
A browser API that runs a callback during the leftover time in a frame after rendering, typically 1-5ms. It is meant for non-urgent work such as prefetching or logging; DOM changes there won't be painted until a later frame.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗