Why is `transform` preferred over animating layout properties
Animating `width`, `top`, `margin`, etc. triggers layout (reflow) and paint on every frame — expensive, runs on the main thread, and janks. `transform` and `opacity` can be handled by the compositor on the GPU: no layout, no paint, just compositing. So they animate at 60fps even under main-thread load. Promote with `will-change`/`transform: translateZ(0)` when needed.
The browser renders in a pipeline, and which stage your animation triggers decides whether it's smooth or janky.
The rendering pipeline
JS → Style → Layout → Paint → Composite- Layout (reflow) — calculate geometry: positions and sizes.
- Paint — fill in pixels: colors, text, borders, shadows.
- Composite — assemble the painted layers into the final image (can run on the GPU).
The earlier the stage you trigger, the more work — because every later stage must re-run too.
Why layout properties are expensive to animate
Animating width, height, top, left, margin, padding triggers Layout → Paint → Composite on every frame. Layout can cascade to siblings and children. At 60fps you have ~16ms per frame; full layout + paint on a complex page blows that budget → dropped frames → jank. And it all happens on the main thread, competing with your JS.
Why transform and opacity are cheap
transform (translate/scale/rotate) and opacity can be animated at the Composite stage only — no layout, no paint. The element is promoted to its own compositor layer, and the GPU just re-composites it at a new position/scale/opacity. This:
- Skips the two most expensive stages.
- Can run off the main thread (on the compositor thread), so it stays smooth even while JS is busy.
/* janky — layout every frame */
.bad { transition: left 0.3s; }
.bad:hover { left: 100px; }
/* smooth — compositor only */
.good { transition: transform 0.3s; }
.good:hover { transform: translateX(100px); }Layer promotion: will-change
To hint the browser to promote an element to its own layer ahead of time:
.animated { will-change: transform, opacity; }(The old trick was transform: translateZ(0).) Use it sparingly — every layer costs GPU memory; promoting everything backfires.
The mapping to remember
| Property animated | Triggers | Cost |
|---|---|---|
width, height, top, margin, left | Layout + Paint + Composite | High — jank |
color, background, box-shadow, border-radius | Paint + Composite | Medium |
transform, opacity | Composite only | Low — smooth |
Senior framing
The senior answer is the pipeline mental model: animation cost is "how far back in JS → Style → Layout → Paint → Composite do you reach." transform/opacity reach only the last stage and can go off-main-thread, which is why they hit 60fps. Add the nuance that will-change is a tool with a memory cost, not a free "make it fast" button.
Follow-up questions
- •What's the difference between reflow, repaint, and composite?
- •What does will-change do and what's the downside of overusing it?
- •How would you animate something that genuinely needs a width change?
Common mistakes
- •Animating top/left/width/margin and wondering why it janks.
- •Slapping will-change on everything, exhausting GPU memory.
- •Thinking all CSS animations are GPU-accelerated.
Edge cases
- •Animating box-shadow is paint-heavy — animate an overlapping pseudo-element's opacity instead.
- •Too many compositor layers hurts more than it helps.
- •transform animations can blur text mid-animation on some GPUs.
Real-world examples
- •Slide-in panels, modals, hover scale effects, parallax, FLIP animations.