The rendering pipeline
The browser turns DOM and CSS into pixels in stages: style, layout, paint, composite. Knowing which a property triggers is the key to smooth, jank-free UI.
In one line: the browser paints your page through a fixed pipeline — style, then layout, then paint, then composite — and the only animations that stay smooth are the ones that enter that pipeline as late as possible.
What it is
Every visual change you make travels through the same four-stage pixel pipeline before it reaches the screen:
- Style — the browser resolves which CSS rules apply to which elements and computes their final values. This runs for any change, however small.
- Layout — it computes the geometry: how big each box is and where it sits relative to everything else. Also called reflow.
- Paint — it fills in pixels: text, colors, borders, shadows, images — rasterized into layers.
- Composite — it hands those layers to the GPU, which assembles them (and applies transforms and opacity) into the final frame.
The pipeline only ever runs forwards. A change that forces an early stage forces every stage after it too: touch layout and you also pay for paint and composite. Touch only composite and the browser skips everything above it. That asymmetry is the whole game.
Why it matters
You have a 16-millisecond budget per frame to hit 60fps. Animating a property that triggers layout means the browser recomputes the geometry of the page — potentially every element — on every single frame, then repaints, then composites. That work rarely fits in 16ms, frames drop, and the result is visible jank.
Animating a compositor-only property is a different category of cheap. transform and opacity don't change geometry or pixels — they only tell the GPU how to assemble layers it already has. The compositor runs on its own thread, so these animations stay smooth even when the main thread is busy. Same visual motion, a fraction of the cost.
See it
Tweak it3
Pick a CSS property and watch which stages light up. The further left the highlight starts, the more expensive the property is to animate — geometry properties reach all the way back to layout, while transform lights only the compositor.
How it works
Each stage does a distinct job, and which one a property reaches determines its cost:
Style runs for every change — it is unavoidable but cheap relative to what follows.
Layout is forced by anything that changes geometry: width, height, top, left, margin, padding, font-size, or adding and removing elements. The browser must recompute positions, so a layout change cascades into paint and composite. This is the expensive rung.
Paint is forced by visual-only changes that don't move boxes: color, background-color, box-shadow, border-radius, visibility. Layout is skipped, but the browser re-rasterizes the affected pixels — and effects like shadows make that rasterization costly.
Composite-only is the privileged path: transform and opacity. No layout, no paint — the GPU just reassembles existing layers. This is the work that fits comfortably inside the 16ms frame budget, because it largely runs off the main thread on the compositor thread.
Build it
The fix for janky motion is almost always the same: stop animating geometry and color, and express the same effect with transform instead. Here a position-and-size animation that reflows every frame becomes a compositor-only one.
/* BEFORE — animating top/width forces layout + paint every frame (janky). */
.card-janky {
position: relative;
transition: top 200ms ease, width 200ms ease;
}
.card-janky:hover {
top: -8px;
width: 320px;
}
/* AFTER — transform stays on the compositor thread (smooth). */
.card-smooth {
/* Hint the browser to promote this to its own layer ahead of time.
Use sparingly: each will-change layer costs GPU memory. */
will-change: transform;
transition: transform 200ms ease;
}
.card-smooth:hover {
transform: translateY(-8px) scale(1.08);
}
/* Always pair motion with a reduced-motion guard. */
@media (prefers-reduced-motion: reduce) {
.card-smooth {
transition: none;
}
}Make it yours
Use the controls beside the demo above to change animate this property, frame-rate target, and compare all properties — each change updates the example live.
Experiment in the playground- Step through every property in the demo and note where the highlight begins — that left edge is the cost.
- Take a transition in your own product that animates
top,left,width, orheightand rewrite it withtranslateandscale. The motion looks identical; the frame cost collapses. - Audit your
will-changeusage: it earns its keep only on elements about to animate. Leaving it on permanently wastes GPU memory for no benefit.
Reproduce it with an LLM
Reproduce it with an LLM
You are a browser-performance engineer. A component animates by transitioning its `width`, `top`, and `box-shadow` on hover and it janks on mid-range phones. Explain why in terms of the rendering pipeline (style → layout → paint → composite): which of those properties force layout, which force paint, and why that's expensive every frame. Then rewrite the animation to run on the compositor only — using `transform` (translate/scale) and `opacity` — keeping the same visual result, plus a `will-change` note (and when NOT to use it) and a `prefers-reduced-motion` guard. Explain each change by the stage it removes. Return the before/after CSS and the explanation.
Pitfalls & accessibility
- Reading layout properties (
offsetTop,getBoundingClientRect) right after writing them forces synchronous layout — "layout thrashing." Batch reads, then writes, to avoid stalling the main thread.