CSSAnimationPerformanceFrontend

Reflow and Repaint: Why CSS Animations Get Janky

Understand browser rendering steps, reflow, repaint, compositing, and why transform and opacity are usually better animation targets than width, height, top, or left.

·Updated ·7 min read·Counting...
Reflow and Repaint: Why CSS Animations Get Janky

When an animation feels janky, people often repeat "use transform and opacity." That advice is usually right, but it is more useful when you understand the rendering pipeline behind it. The goal is to know whether your change forces layout, repaint, or only a cheaper compositing step.

Browser rendering in rough steps

A simplified rendering pipeline:

  1. Calculate styles.
  2. Calculate layout, often called reflow.
  3. Paint pixels.
  4. Composite layers and display.

Performance work becomes clearer when you ask: which step does this CSS property force the browser to revisit?

What reflow means

Reflow is layout recalculation. The browser must work out element sizes, positions, and document flow.

Common triggers include:

  • width and height
  • margin and padding
  • border
  • top and left
  • display
  • inserting, deleting, or moving DOM nodes
  • writing styles and immediately reading layout values such as offsetWidth or scrollHeight

Reflow is often more expensive than repaint because it can affect siblings, parents, and later content.

What repaint means

Repaint means the layout did not change, but pixels must be redrawn.

Examples:

  • color
  • background
  • box-shadow
  • outline

Repaint is usually cheaper than layout, but it is not free, especially for large areas, shadows, filters, or frequent updates.

Why transform and opacity are better animation targets

transform and opacity can often be handled closer to the compositing stage. They usually avoid recalculating document layout and are easier for the browser to animate smoothly.

Compare:

.bad {
  left: 200px;
}

.better {
  transform: translateX(200px);
}

Changing left can pull the element back into layout. transform visually moves the already-laid-out result.

That is why common UI transitions often use:

transition: transform 200ms ease, opacity 200ms ease;

Patterns that cause jank

Animating layout dimensions:

.panel {
  transition: width 300ms ease;
}

If this panel pushes nearby content, every frame may involve layout work.

Alternating layout writes and reads:

element.style.width = "200px"
const height = element.offsetHeight

This can force the browser to flush layout immediately.

Overusing will-change:

.card {
  will-change: transform;
}

will-change is not a default optimization switch. It consumes resources and should be reserved for elements that truly change frequently and benefit from it.

Better optimization habits

  • Animate movement, scale, and fade with transform and opacity.
  • Avoid animations that constantly affect neighboring layout.
  • Batch layout reads and writes instead of interleaving them.
  • Decouple animated overlays from normal document flow when product behavior allows it.
  • Use browser performance tools to confirm the real bottleneck.

If the visual issue includes overlays appearing behind other elements, check Why z-index Fails: Understanding CSS Stacking Context.

A better drawer approach

This works but may be expensive:

.drawer {
  height: 0;
  overflow: hidden;
  transition: height 250ms ease;
}

When possible, keep the outer layout stable and animate an inner element with transform and opacity. That often gives a smoother result and isolates the animation from surrounding layout.

Subscribe to FreeMac

Weekly picks: free Mac software reviews, trusted source updates, alternatives, and low-friction guides.