Siddhant Deval
Siddhant DevalAuthor
Senior Full-Stack Engineer·Aug 22, 2026·11 min read

The Frame Budget & Rendering Pipeline: 16.67 ms to Ship a Frame

Dissect the 60 fps frame budget, walk each phase of the rendering pipeline — Style, Layout, Paint, Composite — and learn how requestAnimationFrame and requestIdleCallback fit into the frame lifecycle.

The Frame Budget & Rendering Pipeline: 16.67 ms to Ship a Frame

Fluid animation is a contract between your code and the display hardware. At 60 Hz, the monitor is ready for a new frame every 16.67 milliseconds. The browser must complete every phase of the rendering pipeline — Style, Layout, Paint, Composite — inside that window or the frame is dropped and the user sees a stutter.
Understanding every phase, what it costs, and which API hooks exist within the frame lifecycle is what allows you to write JavaScript that reliably stays inside the budget.
Horizontal timeline diagram spanning 16.67 ms representing one frame at 60 fps. Eight stages are marked as sequential blocks from left to right: (1) Macrotask, (2) Microtask Checkpoint, (3) requestAnimationFrame callbacks, (4) Style Calculation, (5) Layout, (6) Paint, (7) Composite, (8) requestIdleCallback — shown as an optional right-aligned block only present when budget remains. A red vertical line at the right edge marks the 16.67 ms deadline. A second timeline below it collapses all eight stages into the 8.33 ms window for 120 fps displays.
Figure: Horizontal timeline diagram spanning 16.67 ms representing one frame at 60 fps. Eight stages are marked as sequential blocks from left to right: (1) Macrotask, (2) Microtask Checkpoint, (3) requestAnimationFrame callbacks, (4) Style Calculation, (5) Layout, (6) Paint, (7) Composite, (8) requestIdleCallback — shown as an optional right-aligned block only present when budget remains. A red vertical line at the right edge marks the 16.67 ms deadline. A second timeline below it collapses all eight stages into the 8.33 ms window for 120 fps displays.

1. The Frame Budget

Display Refresh RateTime Per Frame
60 Hz16.67 ms
90 Hz11.11 ms
120 Hz8.33 ms
144 Hz6.94 ms
The budget is fixed by hardware. Your JavaScript, style work, layout, and paint combined must finish inside this window. Any phase that overruns causes a dropped frame — the browser shows the previous frame again, and the user perceives jank.
Crucial Requirement
The browser itself consumes ~1–2 ms of every frame budget for internal overhead (compositing, IPC, display scheduling). Budget-conscious code must target ~12–14 ms, not the full 16.67 ms.

2. The Rendering Pipeline Phases

Phase 1 — Macrotask

The event loop dequeues one task from the Macrotask queue (a setTimeout callback, a click handler, a network callback, etc.) and runs it to completion. This is where the vast majority of JavaScript runs.
Performance / Safety Warning
If your Macrotask runs for 20 ms, the browser misses the 16.67 ms frame deadline before any rendering phase even begins. Long Macrotasks are the most common cause of animation jank. Use scheduler.yield() or setTimeout(fn, 0) chunking to break long work into sub-16 ms slices.

Phase 2 — Microtask Checkpoint

Immediately after the Macrotask finishes, the browser exhaustively drains the Microtask queue: all resolved Promise.then callbacks, queueMicrotask callbacks, and MutationObserver notifications. The pipeline cannot advance until the Microtask queue is empty.

Phase 3 — requestAnimationFrame

If a frame is scheduled for this tick, the browser fires all pending requestAnimationFrame callbacks before touching the DOM for rendering. This makes rAF the only architecturally correct place to perform DOM mutations that need to appear in the current frame.
typescript
// ✅ Correct: batched write inside rAF, before Style/Layout runs
requestAnimationFrame(() => {
  element.style.transform = `translateX(${x}px)`
})

// ❌ Incorrect: modifying DOM in setTimeout has no timing guarantee
// relative to the rendering pipeline
setTimeout(() => {
  element.style.transform = `translateX(${x}px)`
}, 0)

Phase 4 — Style Calculation

The browser matches every CSS selector against every DOM node to compute Computed Styles — the final, cascaded value of every property on every element. This is an O(n × s) operation across nodes and selectors.
Pro Tip & Optimization
Deep, complex selectors (e.g., .nav ul li a:hover span) are matched right-to-left and force the browser to walk large swaths of the subtree. Flat, atomic class selectors (BEM or utility classes) dramatically reduce the style recalculation cost.

Phase 5 — Layout (Reflow)

Given the computed styles, the browser calculates the geometric position and dimensions of every box in the document. This is the most computationally expensive phase for pages with large, interdependent DOM trees.

Layout Thrashing

Layout Thrashing is the single most destructive pattern in interactive UI code. It occurs when JavaScript alternates reads and writes of layout-dependent properties within the same frame:
javascript
// ❌ Layout Thrashing — forces 3 synchronous layouts
const h1 = el1.offsetHeight  // READ → forces layout
el2.style.height = h1 + 'px' // WRITE → invalidates layout
const h2 = el2.offsetHeight  // READ → forces layout again
el3.style.height = h2 + 'px' // WRITE → invalidates layout
const h3 = el3.offsetHeight  // READ → forces layout again
javascript
// ✅ Batched reads, then batched writes — one layout total
const h1 = el1.offsetHeight
const h2 = el2.offsetHeight
const h3 = el3.offsetHeight
el2.style.height = h1 + 'px'
el3.style.height = h2 + 'px'
el4.style.height = h3 + 'px'
Two flame chart panels side by side from Chrome DevTools Performance tab. Left panel shows interleaved purple Recalculate Style and Layout blocks repeating three times in sequence, labeled 'Thrashing — 3x layout in one frame'. Right panel shows a single short Recalculate Style and Layout block, labeled 'Batched — 1x layout in one frame'. The total frame time in the left panel overruns the 16.67 ms deadline; the right panel finishes with budget remaining.
Figure: Two flame chart panels side by side from Chrome DevTools Performance tab. Left panel shows interleaved purple Recalculate Style and Layout blocks repeating three times in sequence, labeled 'Thrashing — 3x layout in one frame'. Right panel shows a single short Recalculate Style and Layout block, labeled 'Batched — 1x layout in one frame'. The total frame time in the left panel overruns the 16.67 ms deadline; the right panel finishes with budget remaining.

Phase 6 — Paint

Paint does not draw pixels to the screen. Instead, the Main Thread generates a Display List (paint records): a serialised list of draw commands — "fill this rectangle with colour #3A7BF4", "stroke this path with 2 px", "draw this glyph at position (120, 44)" — for each changed layer.
This display list is what gets handed to the Compositor Thread for rasterisation.

Phase 7 — Composite

The Compositor Thread receives updated paint records and layer metadata. It dispatches tile rasterisation to Raster Worker Threads (via the GPU Process), then combines all the rasterised layers into the final frame and sends it to the display.
Because transform and opacity changes do not invalidate paint records — they only affect which matrix is applied to an already-rasterised layer — the Compositor can handle those changes independently.

Phase 8 — requestIdleCallback (Optional)

If the browser finishes Composite with remaining time before the next VSync tick, it fires requestIdleCallback with an IdleDeadline object that reports the remaining budget in milliseconds.
typescript
requestIdleCallback((deadline) => {
  while (deadline.timeRemaining() > 0 && workQueue.length > 0) {
    processNextItem(workQueue.shift())
  }
}, { timeout: 2000 })
Performance / Safety Warning
requestIdleCallback is exclusively for non-visual, deferrable work: analytics batching, prefetch queue processing, cache warming. Never perform DOM mutations or anything that affects layout inside an idle callback — the browser may fire it late, after the frame deadline, causing the next frame to thrash.

3. Pipeline Shortcutting

Not every frame requires all six rendering phases. The browser tracks which layers have changed and skips phases whose outputs are still valid:
What changedPhases triggered
transform or opacity onlyComposite only
background-color (no geometry change)Paint → Composite
width, height, marginLayout → Paint → Composite
Inserted/removed DOM nodeStyle → Layout → Paint → Composite
Understanding this table lets you predict the cost of any CSS or DOM change before profiling.

4. References

  1. Rendering Performance — Google Web Fundamentals
  2. requestAnimationFrame — MDN Web Docs
  3. requestIdleCallback — MDN Web Docs
  4. Avoid Large, Complex Layouts and Layout Thrashing — Chrome Developers
  5. CSS Triggers — What layout-triggering properties are
Research & Synthesis Note

This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.

#Browser Internals#Performance#Frame Budget#Layout Thrashing#requestAnimationFrame#requestIdleCallback
Siddhant Deval

Written by Siddhant Deval

Senior Full-Stack Engineer building high-scale architectures, browser performance engineering systems, and SaaS platforms.