Why Only Animate Transform and Opacity for Performance: The Complete Guide

You should only animate transform and opacity because these are the only CSS properties that run entirely on the GPU, bypassing the browser's expensive layout and paint phases to maintain smooth 60fps motion.

Animating the wrong CSS properties can cripple your application's frame rate and introduce visible jank. According to the emilkowalski/skills repository architecture guidelines, restricting animations to transform and opacity is the only reliable path to achieving hardware-accelerated motion without blocking the main thread or forcing costly style recalculations.

Understanding the Browser Rendering Pipeline

Modern browsers render web pages through a multi-step pipeline: layout (calculating geometry), paint (filling pixels), and composite (drawing layers to the screen). Most CSS properties force the browser to execute all three steps on every animation frame. However, transform and opacity are unique compositor-only properties that skip directly to the final composite step on the GPU.

When you animate layout-affecting properties like width, height, margin, or position coordinates, the browser must recalculate the entire page geometry, repaint the affected regions, and then composite. This triple-phase execution saturates the main thread and drops frames under heavy load.

Why Transform and Opacity Are GPU-Only

The emilkowalski/skills repository explicitly documents this performance boundary in skills/review-animations/STANDARDS.md:

"Only animate transform and opacity — they skip layout/paint and run on the GPU. padding/margin/height/width/top/left trigger all three rendering steps."

Transform Properties

The transform property manipulates an element's position, scale, rotation, or 3D orientation entirely within the compositor. Because these geometric changes don't affect the document flow or neighboring elements, the browser can batch these updates on the GPU without consulting the main thread's layout engine.

Opacity Changes

Like transform, opacity adjustments occur during the composite phase. The browser simply adjusts the alpha channel of an existing layer rather than recalculating pixel values across the render tree. This makes opacity transitions extremely cheap even on complex DOM structures.

The Cost of Layout-Triggering Properties

Animating properties that affect layout forces the browser into a performance bottleneck known as layout thrashing. The following table illustrates the rendering cost difference:

Property Rendering Phase Performance Impact
transform Composite only Fast – GPU-only, minimal main-thread work
opacity Composite only Fast – GPU-only, no re-layout
width, height, top, left Layout + Paint + Composite Slow – forces complete render tree recalculation

When animating height or width, the browser must answer geometry questions like "What size is this element now?" and "Where do surrounding elements move?" on every frame. This synchronous layout calculation blocks JavaScript execution and destroys animation smoothness.

Repository Standards and Guidelines

The skills/review-animations/STANDARDS.md file codifies this rule at lines 112-113, establishing it as a hard constraint for all animations in the codebase. This standard ensures that animations remain interruptible and resilient to page load conditions.

Additional constraints appear throughout the repository:

Practical Code Examples

The repository provides specific implementation patterns for both vanilla CSS and React animation libraries.

Correct: GPU-Only CSS Animation

Animate transform and opacity to fade and slide elements without layout impact:

.item {
  opacity: 0;
  transform: translateY(8px);
  animation: fadeIn 300ms ease-out forwards;
}

@keyframes fadeIn {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

Incorrect: Layout-Triggering Animation

Never animate dimensional properties that force reflow:

/* Bad: animating height forces layout + paint on every frame */
.box {
  height: 0;
  animation: grow 400ms ease-out forwards;
}

@keyframes grow {
  to {
    height: 200px;  /* Triggers layout recalculation */
  }
}

Framer Motion Implementation

When using Framer Motion, always use the full transform string rather than shorthand properties:

import { motion } from "framer-motion";

// ✅ Correct: Uses compositor thread
<motion.div animate={{ transform: "translateX(100px)" }} />
// ❌ Incorrect: Runs on main thread via requestAnimationFrame
<motion.div animate={{ x: 100 }} />

The shorthands x, y, and scale execute via JavaScript on the main thread, while the complete transform string leverages the browser's native compositor as documented in skills/review-animations/STANDARDS.md (lines 118-122).

Advanced Performance Considerations

Beyond property selection, the repository identifies two additional anti-patterns that negate GPU acceleration.

Avoid CSS Variable Animation on Parents

Updating CSS custom properties on parent elements forces style recalculation for all descendants, effectively bypassing the GPU optimization. The standards file warns against this pattern at lines 113-117, noting that animating a variable like --x via @property still triggers layout if it affects geometry.

Framer Motion Shorthand Limitations

As noted in the source analysis, Framer Motion's shorthand properties (x, y, scale) bypass the compositor and run through requestAnimationFrame. For high-performance scenarios, always expand these to their transform string equivalents to ensure the animation stays on the GPU.

Summary

  • Only animate transform and opacity to ensure GPU-only compositing and avoid layout/paint costs.
  • Layout properties like width, height, margin, and position coordinates force expensive recalculation of page geometry on every frame.
  • Use full transform strings in animation libraries like Framer Motion rather than shorthand properties to maintain hardware acceleration.
  • Never animate CSS variables on parent elements, as this forces style recalculation across descendant trees and negates GPU benefits.

Frequently Asked Questions

What happens if I animate width or height instead of transform?

Animating width or height forces the browser to execute the complete rendering pipeline—layout, paint, and composite—on every animation frame. This recalculates the position and size of the animated element and potentially its siblings, blocking the main thread and causing visible stutter or "jank" during the animation.

Why are Framer Motion shorthands like x and y slower than transform strings?

Framer Motion shorthands (x, y, scale) are implemented via JavaScript's requestAnimationFrame running on the main thread, while the full transform CSS string is handled natively by the browser's compositor on the GPU. Under heavy load or concurrent animations, the main-thread approach drops frames, whereas the compositor maintains smooth motion.

Can I use will-change to accelerate other properties?

While will-change can hint to the browser to prepare layers for animation, it does not eliminate the layout and paint costs for properties like width or top. The emilkowalski/skills repository standards explicitly reject this optimization for layout properties, insisting that only transform and opacity provide guaranteed GPU-only rendering paths.

How do CSS variables affect animation performance?

Animating CSS custom properties (variables) on parent elements forces the browser to recalculate styles for all descendants, effectively negating the GPU acceleration benefits. Even if the variable controls a transform value, updating the variable itself triggers style invalidation across the subtree, moving work back to the main thread.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →