The Browser's Rendering Pipeline

Before any shader runs, the browser runs a pipeline of its own — style, layout, paint, composite — and the CSS property you animate decides how much of it re-runs every frame. Two properties skip nearly all of it.

Core Idea

The two boxes above are running the same race. One is moved by writing left every frame; the other by writing transform — and until you press make layout expensive, you cannot tell them apart. The browser has a pipeline of its own, wrapped around the GPU one this KB already maps: every visual change flows through style → layout → paint → composite, and the property you touched decides where in that chain the work restarts. left re-enters at layout and drags everything after it; transform enters at the very last stage. Press the button: the page suddenly has real geometry to solve, the left lane starts to stumble — and the transform lane keeps gliding, because by then it is not even running on the thread you just clogged.

Four Stages

Style resolves which CSS rules apply to which elements and computes the final value of every property — the cascade, custom properties, media queries, all of it settled into numbers.

Layout (historically reflow) solves the geometry: given those computed styles, where does every box sit and how big is it? This is the interdependent stage — a width change in one element can move everything after it, which is why layout cost scales with how much of the tree the change can reach, not with the size of the change.

Paint turns boxes into pixels: text, borders, shadows, gradients rasterized into layer bitmaps. This is the browser doing for rectangles and glyphs what rasterization does for triangles — same idea, different geometry.

Composite takes the painted layers and assembles the final frame on the GPU — position this layer here, at this opacity, under this transform. No geometry is solved, no pixels are repainted; existing bitmaps are just placed.

The stages only run forward. Nothing you do in composite re-triggers layout — but anything that touches layout forces paint and composite to follow. That one-way street is the whole cost model of animating the DOM.

What a Property Costs

Every CSS property has an entry point into the chain. width and top change geometry, so they enter at layout and pay for the full ride. color and box-shadow leave geometry alone but change pixels, so they enter at paint. transform and opacity change neither — the compositor can apply both to the already-painted bitmap, so they enter at composite and cost almost nothing. That is the entire craft of cheap UI animation in one sentence: animate the two properties that enter last. It is why every polished motion system — and every card hover on this site — moves things with transform and fades them with opacity, never with top or margin.

Layout Thrash

Layout is lazy: writes only mark geometry dirty, and the browser prefers to solve it once, after your JavaScript finishes. But a read of geometry — offsetHeight, getBoundingClientRect(), even offsetWidth — demands a fresh answer immediately, forcing a synchronous layout in the middle of your code. Alternate reads and writes and you force it over and over:

// a classic thrash loop — layout is forced N times, once per read
for (const el of items) {
  const h = el.offsetHeight          // READ — forces layout for a fresh answer
  el.style.height = `${h + 10}px`    // WRITE — dirties geometry again
}

// same result, one layout: read everything, then write everything
const heights = items.map((el) => el.offsetHeight)   // one layout, N answers
items.forEach((el, i) => { el.style.height = `${heights[i] + 10}px` })

The hero instrument’s left lane commits this sin deliberately — every frame it writes geometry and then reads it back, so the forced layout lands inside the frame’s JavaScript where a meter can catch it. The disciplined version of the same idea is requestAnimationFrame: the browser hands you one aligned moment per frame to do all your writes together, just before the pipeline runs once. Where that moment sits among tasks and microtasks is the event loop’s story.

The Compositor Has Its Own Thread

Composite does not happen on the main thread. The compositor is a separate thread (feeding the GPU process) that holds the painted layers and assembles frames from them — and for transform and opacity animations it can be handed the whole animation, keyframes and all. From then on the motion runs compositor-side: the main thread can be busy solving layout, running the collector, or blocked entirely, and the animation keeps playing. That is what the hero shows when stress is on — the transform lane is a CSS animation running on the compositor, gliding straight through main-thread jank that the left lane, driven by JavaScript, cannot escape.

Layers are the currency here, and they are not free: each composited layer is a bitmap in GPU memory, so promoting everything (will-change sprinkled like salt) trades jank for memory pressure — a trade that goes bad fastest on phones, where mobile-optimization already fights for every megabyte.

Where the Canvas Lives

A <canvas> running WebGL sits in this picture as one composited layer — a rectangle whose contents the browser’s paint stage never looks inside. What fills that rectangle is the other pipeline: vertex shaders, rasterizer, fragment shaders, as mapped in the GPU pipeline. The two articles are siblings by design — this page is the pipeline around the canvas, that page the pipeline inside it — and the frame you finally see is the compositor placing your rendered scene under the DOM sitting on top of it.

That boundary is a real architectural line in shipped work. TIKI DASH renders the whole jungle inside one canvas layer and keeps its HUD — score, hearts, menus — as plain DOM stacked above it. A score tick then costs a repaint of one small text layer and a composite; it can never trigger layout that reaches the world underneath, and the world’s frame budget never pays for the UI. One boundary, two pipelines, each doing only its own work.

Connections

This pipeline is the missing half of every jank diagnosis: cpu-gpu-bound asks which processor is over budget, and this page asks which stage — a dropped frame can be JavaScript, forced layout, a paint storm, or too many layers, and each has a different cure. Paint is the browser’s own rasterization pass, which is why large blurred shadows cost what large triangles cost. The aligned moment where all of this begins each frame — and what happens when your code overstays it — is the event loop. And the one layer the browser never paints inside, the canvas, is where the GPU pipeline takes over.