Performance Optimization: Why You Should Only Animate Transform and Opacity
Animating exclusively transform and opacity keeps rendering on the GPU, bypassing expensive layout and paint calculations to maintain 60 FPS even under heavy browser load.
The emilkowalski/skills repository enforces strict performance guidelines for UI animations, mandating that developers restrict animations to transform and opacity to ensure GPU acceleration and prevent frame drops. This performance optimization strategy eliminates layout thrashing by avoiding properties that trigger the browser's full rendering pipeline.
The Browser Rendering Pipeline and GPU Acceleration
Animations that touch layout-affecting properties—such as width, height, margin, top, or left—force the browser to execute a complete three-step pipeline: layout, then paint, and finally composite. This process is computationally expensive and frequently causes frame drops on busy pages.
In contrast, transform and opacity are GPU-only properties. The browser can skip the layout and paint phases entirely, compositing changes directly on the GPU. According to skills/review-animations/STANDARDS.md, this allows animations to "skip layout/paint and run on the GPU"【112】, delivering consistent 60 fps performance even under load.
Critical Rules from the Skills Repository
Avoid Layout-Triggering Properties
Never animate properties that affect document flow. As documented in skills/improve-animations/AUDIT.md, properties like width, height, margin, padding, top, and left trigger the full rendering pipeline【73】, forcing the browser to recalculate geometry and repaint pixels on every frame.
Framer Motion Anti-Patterns
Do not use Framer Motion's shorthand properties (x, y, scale). According to skills/improve-animations/AUDIT.md, these shorthands are implemented on the main thread via requestAnimationFrame, which defeats GPU acceleration【75】. Instead, use the full transform string:
<motion.div animate={{ transform: "translateX(100px)" }} />
The CSS Variable Trap
Never drive child transforms via a CSS variable on the parent. Updating a CSS variable causes a style recalculation for every descendant, negating the GPU benefit【76】. As noted in skills/improve-animations/AUDIT.md, you should apply the transform directly to the target element rather than updating a variable that multiple children reference.
Practical Implementation Examples
Press Feedback with CSS
For subtle button interactions, use transform and opacity with short transitions:
.button {
transition: transform 160ms ease-out, opacity 160ms ease-out;
}
.button:active {
transform: scale(0.97);
opacity: 0.95;
}
This pattern, referenced in skills/emil-design-eng/SKILL.md【31‑35】, provides tactile feedback without triggering layout calculations.
Popover Animations with Transform Origin
Anchor popovers properly using transform-origin while keeping animations GPU-friendly:
.popover {
transform-origin: var(--transform-origin);
opacity: 0;
transform: scale(0.95);
transition: transform 200ms ease-out, opacity 200ms ease-out;
}
.popover[data-open] {
opacity: 1;
transform: scale(1);
}
The repository defines --transform-origin for trigger-anchored components, noting that modals are the only exception where center is acceptable【112‑113】.
React Patterns with Framer Motion
When using Framer Motion in React, always specify the full transform string:
import { motion } from "framer-motion";
export const SlideIn = () => (
<motion.div
initial={{ opacity: 0, transform: "translateY(20px)" }}
animate={{ opacity: 1, transform: "translateY(0)" }}
transition={{ duration: 0.2, ease: "easeOut" }}
/>
);
Avoid the shorthand x: 20 or scale: 0.95 syntax, as these bypass hardware acceleration.
Direct DOM Manipulation
When updating positions via JavaScript, set the property directly on the element:
// Bad: Updates CSS variable, causing style recalc for all children
container.style.setProperty('--x', `${dx}px`);
// Good: GPU-only transform update
element.style.transform = `translateX(${dx}px)`;
Direct transform updates avoid costly style recalculations for descendant elements【76】.
Summary
- Animate only
transformandopacityto keep rendering on the GPU and skip layout/paint phases. - Avoid layout properties like
width,height,margin, and positional offsets that trigger expensive pipeline calculations. - Use full transform strings in Framer Motion instead of shorthands (
x,y,scale) to ensure hardware acceleration. - Apply transforms directly to animated elements rather than driving them through CSS variables to prevent descendant style recalculations.
- Reference
skills/prototype/PICKER.mdfor the rare intentional exception (width transitions) and its specific safety constraints.
Frequently Asked Questions
Why does animating width or height hurt performance?
Animating width or height forces the browser to run a layout pass on every frame, recalculating the geometry of the element and its siblings, followed by a paint pass to fill the new pixels. This triple-step process (layout → paint → composite) consumes significantly more resources than GPU-only properties, often resulting in frame rates below 60 FPS on complex pages.
Why are Framer Motion's x/y/scale shorthands problematic?
Framer Motion implements shorthand properties like x, y, and scale using requestAnimationFrame on the main thread rather than the compositor thread. According to skills/improve-animations/AUDIT.md, this implementation detail means these animations cannot leverage GPU acceleration, causing them to hitch under load unlike native CSS transforms.
What's wrong with using CSS variables for coordinated animations?
Updating a CSS custom property (variable) to drive child element positions triggers a style recalculation for every descendant selector that references that variable. As documented in skills/improve-animations/AUDIT.md, this global invalidation negates the performance benefits of GPU acceleration, whereas setting transform directly on individual elements limits scope and maintains 60 FPS.
Are there exceptions to the transform and opacity rule?
Yes, but they require explicit justification. The repository references skills/prototype/PICKER.md as an example where width transitions are intentionally permitted with detailed explanations of why the performance tradeoff is acceptable in that specific context. For standard UI components, the rule remains absolute: only transform and opacity.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →