Back to Performance
Performance
medium
mid

Why is transform preferred over animating layout properties for smooth animations?

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.

5 min read·~8 min to think through

The browser renders in a pipeline, and which stage your animation triggers decides whether it's smooth or janky.

The rendering pipeline

ts
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.
css
/* 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:

css
.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 animatedTriggersCost
width, height, top, margin, leftLayout + Paint + CompositeHigh — jank
color, background, box-shadow, border-radiusPaint + CompositeMedium
transform, opacityComposite onlyLow — 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.

Related questions