ESC

Type to search the knowledge base.

transform and opacity Performance

Why transform and opacity animate smoothly — compositor layers, paint vs layout, and when filters and will-change reverse the win.

intermediate3 min read
  • css
  • performance
  • transform
  • opacity

Browsers optimize animations of transform and opacity because they can often run on the compositor without re-laying out the document. Animating width, top, or margin forces layout (and often paint) on every frame — jank city on mobile.

Docs: web.dev animations, MDN transform, rendering performance.

The pipeline (short)

  1. JS / style →
  2. Layout (geometry) →
  3. Paint (pixels into layers) →
  4. Composite (layers GPU-ish)

Transform/opacity changes can skip layout and sometimes skip repainting descendants’ full trees by re-compositing existing layers.

/* Prefer */
.drawer {
  transform: translateX(100%);
  transition: transform 200ms ease;
}
.drawer.is-open {
  transform: translateX(0);
}

/* Avoid for 60fps UI */
.drawer-bad {
  right: -100%;
  transition: right 200ms ease;
}

Opacity for fades

.toast {
  opacity: 0;
  transition: opacity 160ms ease;
}
.toast.is-open {
  opacity: 1;
}

Opacity still creates a stacking context when < 1. Don’t invent extra layers on hundreds of nodes without need.

translateZ / translate3d myths

.card {
  transform: translateZ(0); /* old “force layer” hack */
}

Promoting everything to its own layer increases memory and can hurt. Prefer letting the browser promote when animating, or use will-change briefly (see its article).

What still costs

Change Typical cost
transform, opacity Composite (best case)
filter, large box-shadow changes Paint + composite
width, height, top, left, margin Layout + paint
background-color Paint
Scroll-linked layout reads Layout thrash if mixed with writes
/* Mixed: opacity ok, blur expensive if animated continuously */
.modal-backdrop {
  opacity: 0.5;
  backdrop-filter: blur(8px); /* cost on scroll underneath */
}

FLIP and layout animations

When you must animate layout (reorder lists), measure first (FLIP: First, Last, Invert, Play) and animate only transform between positions. Libraries implement this; the principle is still transform-based motion.

Reduced motion

Performance wins don’t override vestibular safety:

@media (prefers-reduced-motion: reduce) {
  .drawer {
    transition: none;
  }
}

Interview out-loud

“I animate transform and opacity because they can be composited without layout. I avoid animating width/top/margin for continuous UI motion. Layer promotion isn’t free, so I don’t blanket translateZ(0). Filters and backdrop blurs can still be paint-heavy. I verify with Performance panel paint flashing when something janks.”

Footguns

  1. Animating height: auto without a modern strategy.
  2. Infinite blur animations.
  3. Reading layout in scroll handlers every frame.
  4. Too many simultaneous layers on low-end Android.
  5. Using transform for positioning permanently without updating focus/scroll logic (hit testing is fine; document flow isn’t updated).

Measuring jank

  1. Chrome Performance panel → enable screenshots.
  2. Look for long frames during the animation.
  3. Paint flashing / layer borders in Rendering tools.
  4. Compare transform version vs top version of the same drawer.

If the main thread is busy with React render work, compositor-friendly CSS cannot save you — split the problem (defer render, reduce work) separately from paint strategy.

.skeleton {
  opacity: 0.6;
  animation: pulse 1.2s ease-in-out infinite;
}
@keyframes pulse {
  50% { opacity: 0.35; }
}
@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none; opacity: 0.5; }
}

Further reading

Related guides