Home / Articles / Animation & Motion /

animationperformance

Performance: which properties are safe to transition?

The difference between a smooth 60fps animation and a janky one usually comes down to which property you chose to animate. Here's how to pick the right ones.

The rendering pipeline

When a CSS property changes, the browser runs through up to three stages: Layout, Paint, and Composite. The earlier in the pipeline a property triggers work, the more expensive it is to animate.

  • Layout — recalculates geometry (position, size) of the element and its neighbors. Triggers paint and composite too.
  • Paint — redraws pixels (colors, shadows, borders). Triggers composite.
  • Composite — rearranges already-painted layers on the GPU. Cheapest step.

The goal is to animate only composite properties whenever possible.

The safe list: composite-only properties

These properties can be animated entirely on the GPU compositor thread, meaning the main thread stays free for other work.

/* Composite-only — always safe for 60fps */
.element {
  transition:
    transform 0.3s ease,
    opacity 0.3s ease,
    filter 0.3s ease;
}

/* All movement via transform */
.card:hover {
  transform: translateY(-4px) scale(1.02);
  opacity: 0.95;
  filter: brightness(1.1);
}
transform, opacity, and filter are the three properties you can always animate without worry. They skip layout and paint entirely.

The caution list: paint-triggering properties

These properties skip layout but still require the browser to repaint pixels. They're acceptable for small elements or short durations, but can cause jank on large areas.

/* Paint-only — moderate cost */
.btn {
  background: oklch(0.52 0.22 265);
  color: oklch(1 0 0);
  box-shadow: 0 2px 8px oklch(0% 0 0 / 0.12);
  transition:
    background 0.2s ease,
    color 0.2s ease,
    box-shadow 0.25s ease;
}

.btn:hover {
  background: oklch(0.48 0.24 265);
  color: oklch(0.98 0.01 260);
  box-shadow: 0 8px 24px oklch(0% 0 0 / 0.2);
}

Properties in this tier include background-color, color, border-color, box-shadow, and outline. They're fine for buttons and small components — avoid them on full-screen elements.

The avoid list: layout-triggering properties

These properties force the browser to recalculate the position and size of multiple elements. Animating them causes reflow — the most expensive operation.

/* AVOID — these trigger layout on every frame */
.panel {
  transition:
    width 0.3s ease,     /* layout */
    height 0.3s ease,    /* layout */
    padding 0.3s ease,   /* layout */
    margin 0.3s ease,    /* layout */
    top 0.3s ease,       /* layout */
    left 0.3s ease;      /* layout */
}

/* PREFER — use transform equivalents */
.panel {
  transition: transform 0.3s ease;
}

.panel:hover {
  /* Instead of width/height, use scale */
  transform: scale(1.05);
  /* Instead of top/left, use translate */
  /* transform: translate(10px, -5px); */
}

The layout-triggering properties include width, height, padding, margin, top, right, bottom, left, border-width, and font-size.

will-change: use sparingly

will-change hints to the browser that a property will animate soon, allowing it to promote the element to its own compositor layer in advance.

/* Apply only when animation is imminent */
.card:hover {
  will-change: transform;
}

/* Or toggle via a class before animation starts */
.card.will-animate {
  will-change: transform, opacity;
}

/* AVOID — permanent will-change wastes GPU memory */
.card {
  will-change: transform; /* Don't do this globally */
}

Each will-change element gets its own GPU layer, consuming video memory. On a page with hundreds of cards, permanent will-change can crash mobile browsers.

Only apply will-change right before the animation — on :hover, or via JavaScript just before adding an animation class. Remove it afterward.

Measuring with DevTools

Don't guess which properties are expensive — measure. Chrome DevTools provides direct visibility into rendering costs.

  • Performance panel: Record an interaction and look for long frames (red bars). Expand "Rendering" to see layout and paint times.
  • Layers panel: Shows which elements have their own compositor layers and why. Elements with transform or will-change get promoted.
  • Rendering tab: Enable "Paint flashing" to see green overlays on repainted areas. If your entire page flashes green during an animation, you're animating a paint property on a large element.
/* If DevTools shows layout thrashing, refactor like this: */

/* Before — animating height triggers layout */
.drawer {
  height: 0;
  transition: height 0.3s ease;
}
.drawer.open { height: 200px; }

/* After — animating transform skips layout */
.drawer {
  transform: scaleY(0);
  transform-origin: top;
  transition: transform 0.3s ease;
}
.drawer.open { transform: scaleY(1); }

Quick reference table

Use this as a cheat sheet when choosing which properties to animate.

  • Composite (safe): transform, opacity, filter, backdrop-filter, clip-path (in some browsers)
  • Paint (moderate): background, color, box-shadow, border-color, outline, text-decoration-color
  • Layout (avoid): width, height, padding, margin, top/right/bottom/left, border-width, font-size, gap

When in doubt, animate transform and opacity. They cover the vast majority of UI motion — movement, scaling, rotation, and fading — all on the compositor.