interviewpath
  • 0day streak
  • 10total XP
  • Level 1

Focus mode

Browser runtime: event loop, rendering and promises

Read alongside

JavaScript execution model (MDN)

Agents, the job queue and the event loop as JavaScript defines them; read before predicting promise ordering.

Objectives

  • Describe the event loop processing model: one task, then a microtask checkpoint, then a rendering opportunity.
  • Predict the output order of code mixing synchronous logs, setTimeout, promise callbacks, await, queueMicrotask and requestAnimationFrame.
  • Explain the rendering pipeline (style, layout, paint, composite) and which property changes skip which steps.
  • Identify long tasks and forced synchronous layout in a DevTools Performance recording and propose fixes.
  • Choose between chunking with yields, requestAnimationFrame and a Web Worker for a given expensive operation.

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.

Self-check before you finish

  1. After a task finishes, what runs first: pending microtasks, timers or rendering?
  2. Why can an endless chain of microtasks freeze a page while an endless chain of setTimeout calls does not?
  3. What happens to the rest of an async function when it reaches await?
  4. Which changes trigger layout, which only paint, and which can be composited?
  5. What is forced synchronous layout, and how do you avoid it?
  6. What can a Web Worker not do that main-thread code can?