Unit 2 · Software engineering · Lesson 13 of 15
Browser runtime: event loop, rendering and promises
Builds on
Step 1: Learn the ideas
- Main thread, tasks and task sources
- Microtasks: promise reactions, queueMicrotask, MutationObserver
- Event loop processing model and rendering opportunities
- requestAnimationFrame and frame budgets
- Rendering pipeline: style, layout, paint, composite
- Forced synchronous layout and layout thrashing
- Long tasks and yielding to the main thread
- Web Workers for CPU-heavy work
Why it matters
Most frontend performance complaints, such as a button that feels stuck, a frozen spinner or a list that janks while scrolling, trace back to the main thread being busy when the browser needed it. Interviewers use event-loop ordering puzzles to check whether you understand promises precisely, and rendering questions to see whether you can reason about cost instead of guessing.
Core ideas
One thread, several queues. A page's JavaScript, event handlers, style calculation and layout share the main thread. The event loop repeatedly takes one task (a script, a timer callback, an input event, a message), runs it to completion, then performs a microtask checkpoint that drains the microtask queue completely, including microtasks queued by other microtasks. Promise reactions, await continuations, queueMicrotask callbacks and MutationObserver callbacks are microtasks; timers, postMessage and most events are tasks. When the browser decides to render a frame, it runs requestAnimationFrame callbacks and then style, layout and paint.
console.log('1');
setTimeout(() => console.log('5'), 0);
Promise.resolve().then(() => console.log('3'));
(async () => { console.log('2'); await null; console.log('4'); })();
// logs 1 2 3 4 5
Synchronous code finishes first, microtasks run next in the order they were queued, and the timer runs in a later task despite its zero delay. A microtask that keeps re-queuing itself starves rendering and input completely; a chain of timers does not, because the browser regains control between tasks.
The rendering pipeline. After DOM or style changes, the browser recalculates styles, runs layout to compute geometry, paints drawing commands for layers and composites the layers, often on a separate compositor thread using the GPU. Changing geometry (width, position, font size) triggers layout and everything after it; changing only a color skips layout; changing transform or opacity on an element with its own layer can often be handled by the compositor alone, which is why such animations stay smooth while the main thread is busy.
Forced synchronous layout. Reading a layout-dependent value such as offsetHeight or getBoundingClientRect() after a style change forces layout to run immediately. Interleaving writes and reads in a loop (layout thrashing) repeats that work on every iteration; do all reads first and all writes after, or defer the writes to requestAnimationFrame.
Long tasks. A task that occupies the main thread for 50 ms or more counts as a long task, because input arriving during it must wait. At 60 Hz a frame lasts about 16.7 ms, and script, style, layout and paint share that budget. Split work into chunks and yield between them (setTimeout, or scheduler.yield() where supported), do less work per update, or move pure computation to a Web Worker, which has its own thread and event loop but no DOM access.
Common misconceptions
- "
setTimeout(fn, 0)runs right after the current code." It queues a task: pending microtasks, and possibly a rendering step, come first, and browsers enforce a 4 ms minimum once timers are nested deeply enough. - "Async code runs in parallel." Promises and async functions interleave on one thread; only workers and the browser's internal threads (network, decoding, compositing) run in parallel with your script.
- "Microtasks only run at the end of a task." A checkpoint also happens whenever the JavaScript stack empties after a callback, for example between two listeners handling a real user click.
- "requestAnimationFrame fires every 16 ms." It runs before the next frame the browser renders, follows the display's refresh rate and is paused in most browsers for background tabs; use its timestamp argument rather than assuming an interval.
- "Any CSS animation is cheap." Animating layout-affecting properties runs layout and paint on the main thread every frame; transforms and opacity are the dependable compositor path.
How to approach it
Learn the processing model once from a precise source, then rely on experiments: record a DevTools Performance trace of a deliberately slow handler, find the long task and fix it. Graduate-level questions are usually ordering puzzles; mid-level and senior frontend interviews describe a janky interaction and ask where the time goes and where the work should run instead.
Suggested order
- 1Read the core MDN "JavaScript execution model" page, then the "Event loops" section of the HTML Standard through the processing model, skimming the algorithm steps.
- 2Read MDN "Using microtasks in JavaScript with queueMicrotask()" and its in-depth companion; write ten ordering puzzles mixing setTimeout, promises, await, queueMicrotask and requestAnimationFrame, predict each, then run them in the browser console.
- 3Read web.dev "Rendering performance" and "Avoid large, complex layouts and layout thrashing"; build a page that interleaves style writes and offsetHeight reads in a loop, record it in the DevTools Performance panel, then batch the reads and compare.
- 4Read web.dev "Optimize long tasks"; split a 300 ms synchronous loop into chunks that yield and confirm in a trace that clicks are handled between chunks.
- 5Move the same computation into a Web Worker and compare main-thread activity in a new recording.
- 6Solve the Promises and Time section of LeetCode 30 Days of JavaScript to rehearse timer and promise semantics.
Step 2: Check off what you can do
Tap each one you can genuinely do or explain. Each earns 10 XP.
Step 3: Go deeper
JavaScript execution model (MDN)Start here
MDN Web DocsAgents, the job queue and the event loop as JavaScript defines them; read before predicting promise ordering.
Open(opens in a new tab)Link responded, 5 Oct 2026HTML Living Standard: Event loops
WHATWGSection: Event loop processing model
Authoritative order of task selection, microtask checkpoints and the rendering update; skim the algorithm steps.
Open(opens in a new tab)Link responded, 5 Oct 2026MDN: Using microtasks in JavaScript with queueMicrotask()
MDN Web DocsTasks versus microtasks with runnable examples, and the in-depth page on execution contexts and event loop types.
Open(opens in a new tab)Link responded, 5 Oct 2026web.dev: Rendering performanceStart here
Google Chrome team (web.dev)Section: Rendering performance
The style, layout, paint and composite pipeline; then the layout-thrashing, compositor-only and long-task articles listed under this resource.
Open(opens in a new tab)Link responded, 5 Oct 2026Chrome DevTools: Analyze runtime performance
Google (developer.chrome.com)Section: Analyze runtime performance
Recording traces with CPU throttling and reading long tasks and forced reflows in the flame chart.
Open(opens in a new tab)Link responded, 5 Oct 2026
Step 4: Practice
Use the Promises and Time section of 30 Days of JavaScript (Sleep, Timeout Cancellation, Promise Time Limit, Debounce) for timer and promise semantics. The main practice is hands-on: (1) write ten ordering puzzles, predict each, then run it; (2) build a page whose click handler runs for 300 ms, record it in the DevTools Performance panel with CPU throttling, and fix it by chunking with yields or moving the work to a worker; (3) create layout thrashing on purpose, find the forced-reflow warnings in the trace, and remove them by batching reads before writes.
- 30 Days of JavaScript (official LeetCode study plan)LeetCode(opens in a new tab)Not attemptedLeetCode
Record attempts on each problem’s page (+15 XP, +10 more when solved on your own).
Step 5: Test yourself
Answer each one out loud or on paper before you finish the lesson.
- Question 1After a task finishes, what runs first: pending microtasks, timers or rendering?
- Question 2Why can an endless chain of microtasks freeze a page while an endless chain of setTimeout calls does not?
- Question 3What happens to the rest of an async function when it reaches await?
- Question 4Which changes trigger layout, which only paint, and which can be composited?
- Question 5What is forced synchronous layout, and how do you avoid it?
- Question 6What can a Web Worker not do that main-thread code can?
You’re done when you can
- Predict the output of three unseen event-loop ordering puzzles correctly and explain each step.
- Explain the style, layout, paint and composite steps and name a property change that skips layout.
- Find a long task and a forced synchronous layout in a Performance recording of your own page and fix both.
- Explain when a Web Worker is the right fix and when chunking on the main thread suffices.
Finish the lesson
Unlocks next: Frontend state, request races and client-server interfaces, Accessibility and web performance


