React Animations & Performance
Question 1: How to implement animations in React? (CSS transitions, Framer Motion, React Spring)
Main Answer:
Animations in React can be implemented through three primary approaches:
1. CSS Transitions/Animations: The simplest approach using native CSS. Apply styles conditionally based on React state, and CSS handles the animation:
// CSS class with animation
const fadeIn = keyframes`
from { opacity: 0; }
to { opacity: 1; }
`;
const AnimatedDiv = styled.div`
animation: ${fadeIn} 0.5s ease-in;
`;
// React component
function CSSTransitionExample() {
const [show, setShow] = useState(false);
return (
<div className={show ? 'visible' : 'hidden'}>
Content
<button onClick={() => setShow(!show)}>Toggle</button>
</div>
);
}
2. Framer Motion: A declarative motion library that abstracts complex animations:
import { motion } from 'framer-motion';
function FramerExample() {
return (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
Animated content
</motion.div>
);
}
3. React Spring: Physics-based animations for natural motion:
import { useSpring, animated } from '@react-spring/web';
function SpringExample() {
const spring = useSpring({
from: { opacity: 0 },
to: { opacity: 1 },
config: { tension: 280, friction: 60 }
});
return <animated.div style={spring}>Content</animated.div>;
}
Key Differences:
- CSS: Lightweight, GPU-accelerated, but limited expressiveness
- Framer Motion: Easy to use, great for common animations, orchestration support
- React Spring: Physics-based, smooth natural feel, complex setups
Each approach optimizes differently—CSS is best for simple transitions, Framer Motion for UI animations, and React Spring for interactive, gesture-driven animations.
<details> <summary><strong>🔍 Deep Dive: Animation Implementation and Performance</strong></summary>
CSS Transitions Internals:
CSS transitions leverage the browser's animation engine, which runs on a separate thread from JavaScript:
// Browser processes this efficiently
.element {
transition: all 300ms ease;
opacity: 1;
transform: translateX(0);
}
.element.active {
opacity: 0.5;
transform: translateX(100px);
}
When you change the class, the browser calculates intermediate values at 60fps (16.67ms intervals). The critical insight: only properties that don't trigger layout recalculations should be animated—opacity and transform are GPU-accelerated, while width, height, and top cause layout thrashing.
Framer Motion Internals:
Framer Motion uses requestAnimationFrame (RAF) under the hood and applies updates directly to the DOM:
// Simplified RAF implementation (Framer Motion does this)
let animationId;
const animate = (from, to, duration) => {
const startTime = performance.now();
const update = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Easing function applied
const easedProgress = easeInOut(progress);
const value = from + (to - from) * easedProgress;
updateDOM(value);
if (progress < 1) {
animationId = requestAnimationFrame(update);
}
};
animationId = requestAnimationFrame(update);
};
Framer Motion's power comes from:
- Orchestration: Animate multiple elements with
staggerChildren,delayChildren - Gesture support:
whileHover,whileTapfor interactive animations - Layout animations:
layoutIdfor shared layout animations across different components - Variants: Reusable animation definitions
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2
}
}
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
};
// Usage
<motion.ul variants={containerVariants} initial="hidden" animate="visible">
{items.map(item => (
<motion.li key={item.id} variants={itemVariants}>
{item.name}
</motion.li>
))}
</motion.ul>
React Spring Internals:
React Spring differs fundamentally—it's physics-based rather than time-based:
// Physics simulation replaces duration
const spring = useSpring({
from: { opacity: 0 },
to: { opacity: 1 },
config: {
tension: 280, // Spring force
friction: 60, // Resistance
mass: 1, // Object weight
clamp: false // Stop at target
}
});
// Internal calculation (simplified)
// F = -k*x - c*v (Hooke's Law + damping)
// acceleration = F / mass
// velocity += acceleration * deltaTime
// position += velocity * deltaTime
This physics approach creates naturally decelerating animations—the element overshoots the target and settles, mimicking real-world motion. Preset configs exist: config.default, config.gentle, config.wobbly, config.stiff.
GPU Acceleration Details:
Only specific properties trigger GPU acceleration:
/* GPU Accelerated (Good) */
transform: translateX(100px); /* Composite layer */
transform: scale(1.2); /* Composite layer */
opacity: 0.5; /* Composite layer */
will-change: transform, opacity; /* Hint to browser */
/* NOT GPU Accelerated (Bad for animation) */
width: 200px; /* Triggers layout */
height: 100px; /* Triggers layout */
top: 50px; /* Triggers layout */
left: 100px; /* Triggers layout */
margin: 10px; /* Triggers layout */
padding: 20px; /* Triggers layout */
When you animate non-accelerated properties, the browser repaints and recalculates layout constantly, causing jank. The browser's critical rendering path:
- JavaScript execution (your animation code)
- Style calculation (apply CSS)
- Layout (position elements)
- Paint (raster pixels)
- Composite (combine layers)
GPU acceleration skips steps 3-4 by creating a composite layer, letting the GPU handle layer composition directly.
will-change Optimization:
// Tell browser to optimize for upcoming changes
const style = {
willChange: 'transform, opacity',
transform: `translateX(${x}px)`,
opacity: opacity
};
// Browser creates composite layer earlier
// Don't overuse—each layer costs memory
Library Performance Comparison:
- CSS Transitions: ~0.5KB gzipped, sync with browser, perfect 60fps for GPU properties
- Framer Motion: ~42KB gzipped, variable performance based on complexity
- React Spring: ~28KB gzipped, physics calculations add CPU overhead but feel superior
</details>
<details> <summary><strong>🐛 Real-World Scenario: E-Commerce Animation Debugging</strong></summary>
Scenario: E-commerce site with 200+ product cards. When category changes, cards animate from old position to new position. Client reports "animation feels laggy—drops to 30fps, freezes main content."
Debugging Steps:
- Measure baseline performance:
// Performance monitoring
performance.mark('animation-start');
// Animate...
performance.mark('animation-end');
performance.measure('animation', 'animation-start', 'animation-end');
// Check DevTools Performance tab: expect ~16.67ms per frame
// If frame time > 33ms, you're below 30fps
- Identify the problem (wrong implementation):
// WRONG: Animating layout properties
function ProductCard({ item, isAnimating }) {
return (
<div
style={{
height: isAnimating ? '500px' : '400px', // BAD: Layout
width: isAnimating ? '300px' : '200px', // BAD: Layout
left: isAnimating ? '100px' : '0px', // BAD: Layout
transition: 'all 0.3s ease'
}}
>
{item.name}
</div>
);
}
This causes 200 layout recalculations per frame for all cards!
- Actual metrics from real production bug:
Performance Timeline:
- Frame 1: JavaScript: 2ms, Style: 8ms, Layout: 120ms (200 cards × 0.6ms each), Paint: 50ms, Composite: 5ms
- Total: 185ms per frame (5.4 fps) ❌
- Fix: Use transform instead:
// CORRECT: GPU-accelerated properties only
function ProductCard({ item, scale, translateX }) {
const [ref, setRef] = useState(null);
useEffect(() => {
if (ref) {
ref.style.willChange = 'transform';
ref.style.transform = `translate(${translateX}px) scale(${scale})`;
}
}, [translateX, scale, ref]);
return (
<div ref={setRef} style={{ transition: 'transform 0.3s ease' }}>
{item.name}
</div>
);
}
// Or with Framer Motion (handles this internally)
function ProductCard({ item, x, scale }) {
return (
<motion.div
style={{
x: useMotionValue(0),
scale: useMotionValue(1)
}}
animate={{ x, scale }}
transition={{ duration: 0.3 }}
>
{item.name}
</motion.div>
);
}
- Performance improvement:
Performance Timeline (Fixed):
- Frame 1: JavaScript: 2ms, Style: 5ms, Layout: 0ms (no layout!), Paint: 0ms (no paint!), Composite: 3ms
- Total: 10ms per frame (100fps) ✅
Additional Real-World Issues:
Issue: Memory leaks with RAF animations
// WRONG: Doesn't clean up
function AnimatedElement() {
useEffect(() => {
let animationId;
const animate = () => {
animationId = requestAnimationFrame(animate);
// Update DOM...
};
animate();
// Missing cleanup!
}, []);
}
// CORRECT: Clean up RAF
function AnimatedElement() {
useEffect(() => {
let animationId;
const animate = () => {
animationId = requestAnimationFrame(animate);
};
animate();
return () => {
cancelAnimationFrame(animationId); // Cleanup
};
}, []);
}
Issue: Thrashing with rapid state updates
// WRONG: Re-renders animation 100 times/second
function BadAnimation() {
const [position, setPosition] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setPosition(p => p + 1); // Re-renders component!
}, 1000 / 100);
return () => clearInterval(interval);
}, []);
}
// CORRECT: Use ref to track position without re-renders
function GoodAnimation() {
const positionRef = useRef(0);
const elementRef = useRef(null);
useEffect(() => {
let animationId;
const animate = () => {
positionRef.current += 1;
if (elementRef.current) {
elementRef.current.style.transform = `translateX(${positionRef.current}px)`;
}
animationId = requestAnimationFrame(animate);
};
animate();
return () => cancelAnimationFrame(animationId);
}, []);
return <div ref={elementRef}>Animated</div>;
}
</details>
<details> <summary><strong>⚖️ Trade-offs: Animation Approaches Comparison</strong></summary>
CSS Transitions vs JavaScript Animations:
| Aspect | CSS Transitions | JS Animations (RAF) | Framer Motion |
|---|---|---|---|
| Performance | 60fps for GPU properties | Manual optimization needed | ~50-60fps (library overhead) |
| Control | Limited to easing functions | Full frame-by-frame control | High-level control |
| Browser Support | Excellent (IE9+) | Native | Modern browsers |
| Bundle Size | 0KB | 0KB | +42KB |
| Interactivity | None (pre-defined) | Can respond to events | Gesture-aware |
| Orchestration | Complex (CSS + classes) | Manual sequencing | Built-in (variants, delays) |
| Learning Curve | Low | Medium | Medium |
| Motion Physics | Easing functions only | Custom possible | Built-in springs |
When to Use Each:
// CSS Transitions: Simple, performant, predictable
// Use for: Hover effects, state toggles, simple fades
.button:hover {
background: blue;
transition: background 0.3s ease;
}
// useTransition (React 18+): State-driven, built into React
// Use for: Form submissions, data fetching with optimistic UI
function SearchComponent() {
const [isPending, startTransition] = useTransition();
return (
<form onSubmit={(e) => {
startTransition(() => {
// Handles animation of pending state
});
}}>
{isPending && <Spinner />}
</form>
);
}
// Framer Motion: Complex orchestration, multiple elements
// Use for: Page transitions, staggered lists, shared layout animations
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{/* Paired with AnimatePresence for unmounting animations */}
</motion.div>
// React Spring: Physics-based, interactive
// Use for: Gesture-driven animations, drag interactions, natural motion
const bind = useGesture({
onDrag: ({ down, mx }) => {
api.start({ x: down ? mx : 0 });
}
});
<animated.div {...bind()} style={spring} />
Performance Characteristics:
// Measure library overhead
const start = performance.now();
// CSS: No overhead
.element { transition: all 0.3s; }
document.querySelector('.element').classList.add('active');
// Framer Motion: ~2-3ms overhead per animation
<motion.div animate={{ x: 100 }} />
// React Spring: ~3-5ms overhead (physics calculations)
const spring = useSpring({ x: 100 });
Orchestration Complexity:
// CSS: Difficult
// Stagger 10 items with 100ms delay each = 1000ms total
// Must handle with nth-child selectors or inline styles
// Framer Motion: Easy
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map(item => (
<motion.li variants={itemVariants} key={item.id}>
{item.name}
</motion.li>
))}
</motion.ul>
// React Spring: Medium
// Need to coordinate multiple springs manually
const springs = items.map((_, i) =>
useSpring({ delay: i * 100, opacity: 1 })
);
Interactivity Comparison:
// CSS: No response to user input mid-animation
// (Animation runs regardless of user actions)
// RAF: Full control
document.addEventListener('mousemove', (e) => {
element.style.transform = `translateX(${e.clientX}px)`;
});
// Framer Motion: Built-in gestures
<motion.div
whileHover={{ scale: 1.1 }} // Responds while hovering
whileTap={{ scale: 0.95 }}> // Responds while clicking
Hover or tap me
</motion.div>
// React Spring: Gesture support with integration
const [{ x }, api] = useSpring(() => ({ x: 0 }));
bind = useGesture({
onMouseMove: ({ clientX }) => {
api.start({ x: clientX });
}
});
💬 Explain to Junior
Analogy for Animation Methods:
Imagine you want to move a box across the room:
CSS Transitions = Telling your friend "Walk to that corner at a steady pace, and you'll arrive in 5 seconds." They handle the movement themselves. Simple, efficient, works great for straightforward moves.
JavaScript RAF = You tracking your friend with a stopwatch, telling them exactly where to stand every 0.016 seconds. You have complete control—you can change the path mid-movement, respond to obstacles, etc. But you must be very precise or the movement looks jerky.
Framer Motion = Giving your friend a choreographed dance routine. You describe the beginning, the end, and the style ("wave at people while walking"). The library figures out all the intermediate steps. You can orchestrate multiple people doing different things with timing.
React Spring = Using a spring-loaded system—you push the box, and it naturally bounces and settles from momentum. No need to specify timing; physics handles it. Feels more natural because objects naturally decelerate.
Simple Animation Example Explained:
// The Problem: Make a button scale up on hover
// (We'll show all three approaches)
// APPROACH 1: CSS (Simplest)
// HTML: <button className="scaleButton">Hover me</button>
// CSS:
.scaleButton {
transform: scale(1); // Start at normal size
transition: transform 0.3s ease; // Change over 0.3 seconds
}
.scaleButton:hover {
transform: scale(1.1); // End at 110% size
}
// Browser automatically draws 18 frames (0.3s at 60fps) between 1.0 and 1.1
// APPROACH 2: JavaScript RAF (More Control)
function setupAnimation(button) {
button.addEventListener('mouseenter', () => {
let scale = 1;
const targetScale = 1.1;
function animate() {
scale += (targetScale - scale) * 0.2; // Ease toward target
button.style.transform = `scale(${scale})`;
if (Math.abs(scale - targetScale) > 0.001) {
requestAnimationFrame(animate);
}
}
animate();
});
button.addEventListener('mouseleave', () => {
button.style.transform = 'scale(1)';
});
}
// Why this is better: You can interrupt, respond to events, create custom easing
// APPROACH 3: Framer Motion (Developer Experience)
import { motion } from 'framer-motion';
function ScaleButton() {
return (
<motion.button
whileHover={{ scale: 1.1 }}
transition={{ type: 'spring', stiffness: 400 }}
>
Hover me
</motion.button>
);
}
// Why this is better: Readable, reusable, handles complex orchestration
Interview Answer Template:
"There are three main ways to animate in React, each with tradeoffs:
CSS Transitions are best for simple, predictable animations—they're lightweight and always smooth. You change a class or inline style, CSS handles the animation automatically. [Example: button hover scale]
JavaScript with requestAnimationFrame gives you full frame-by-frame control. You update the DOM in sync with the browser's refresh rate—typically 60 times per second. This lets you respond to user input or complex logic, but requires careful optimization to avoid jank.
Libraries like Framer Motion sit in between. You declaratively describe animations using variants and transitions. The library handles RAF under the hood, handles orchestration of multiple elements, and supports gestures like whileHover. It's easier than raw JavaScript but more powerful than CSS alone.
In production, I'd use CSS for simple cases, Framer Motion for UI animations requiring coordination, and React Spring for interactive, physics-based animations. The key is using GPU-accelerated properties like transform and opacity—never animate layout properties like width or left, as these trigger expensive recalculations."
</details>
<details> <summary><strong>💬 Explain to Junior: Animation Methods Explained</strong></summary>
Common Beginner Mistakes:
- Animating layout properties
// WRONG: Causes jank
style={{ width: animatedWidth }}
// RIGHT: Use transform
style={{ transform: `scaleX(${widthRatio})` }}
- Re-rendering component on every animation frame
// WRONG: State update triggers re-render every frame
setInterval(() => setPosition(p => p + 1), 16);
// RIGHT: Update ref without re-rendering
useEffect(() => {
let animationId = requestAnimationFrame(() => {
positionRef.current += 1;
element.style.transform = `translateX(${positionRef.current}px)`;
});
}, []);
- Forgetting to specify transition properties
// WRONG: Changes happen instantly
<div onClick={() => setExpanded(!expanded)}
style={{ width: expanded ? '500px' : '100px' }}>
Content
</div>
// RIGHT: Add transition
<div onClick={() => setExpanded(!expanded)}
style={{
width: expanded ? '500px' : '100px',
transition: 'width 0.3s ease' // Add this!
}}>
Content
</div>
</details>
Question 2: What are performance considerations for React animations?
Main Answer:
React animations present unique performance challenges because they must synchronize with both React's rendering cycle and the browser's animation frame timing. The primary considerations are:
1. Layout Thrashing: Animating properties that trigger layout calculations (width, height, left, top, margin) forces the browser to recalculate element positions repeatedly. A 60fps animation on 100 elements means 6,000 layout calculations per second.
// BAD: Triggers layout
<div style={{ left: position }}>Content</div>
// GOOD: GPU-accelerated
<div style={{ transform: `translateX(${position}px)` }}>Content</div>
2. Re-render Overhead: Each state update in React triggers a render. Animating via state causes excessive re-renders:
// BAD: 60 re-renders/second
setInterval(() => setX(x => x + 1), 16);
// GOOD: Update DOM directly via ref
useEffect(() => {
let animationId = requestAnimationFrame(() => {
if (ref.current) {
ref.current.style.transform = `translateX(${xRef.current}px)`;
}
});
}, []);
3. GPU Utilization:
Only specific properties benefit from GPU acceleration: transform, opacity, and filter. Others like width, height, box-shadow don't, and animating them causes CPU-intensive operations.
4. will-change Abuse:
will-change tells the browser to prepare for animation, but creates memory overhead. Using it on too many elements degrades performance.
// GOOD: Use sparingly on elements that will animate
.animated-element {
will-change: transform, opacity;
}
// BAD: Using on everything
.* { will-change: transform; } // Memory nightmare
5. Event Handler Frequency:
Listeners like onMouseMove fire potentially thousands of times per second. Unoptimized handlers cause jank.
// BETTER: Throttle or use Framer Motion's gesture support
function DragElement() {
const [x, setX] = useState(0);
const updateRequested = useRef(false);
const handleMouseMove = (e) => {
if (!updateRequested.current) {
updateRequested.current = true;
requestAnimationFrame(() => {
setX(e.clientX);
updateRequested.current = false;
});
}
};
return <div onMouseMove={handleMouseMove}>Drag me</div>;
}
The key principle: Minimize re-renders, maximize GPU acceleration, use refs for continuous updates.
<details> <summary><strong>🔍 Deep Dive: Layout Thrashing and Performance Optimization</strong></summary>
Layout Thrashing Mechanics:
The browser's rendering pipeline must complete in strict order:
JavaScript → Style Calculation → Layout → Paint → Composite
(Your (Compute CSS rules) (Calc (Raster (Combine
code) positions) pixels) layers)
When you animate a layout property, you force the browser to repeat steps 2-4 for every frame:
// This code forces layout thrashing
for (let i = 0; i < 100; i++) {
elements[i].style.left = positions[i]; // Triggers layout recalc
const height = elements[i].offsetHeight; // Reads layout (forces flush!)
}
// Browser actually does:
// 1. Write: elements[0].style.left = 100 (queued)
// 2. Read: elements[0].offsetHeight (forces layout flush!)
// Browser: "I need to calculate layout to answer this!"
// 3. Write: elements[1].style.left = 200 (queued again)
// 4. Read: elements[1].offsetHeight (flush again!)
// ...
// Result: 100 layout recalculations instead of 1!
Solution: Batch reads and writes:
// WRONG: Interleaved reads/writes
for (let i = 0; i < 100; i++) {
elements[i].style.left = positions[i]; // Write
const height = elements[i].offsetHeight; // Read (flush!)
}
// RIGHT: Batch all reads, then all writes
const heights = elements.map(el => el.offsetHeight); // All reads at once
elements.forEach((el, i) => {
el.style.left = positions[i]; // All writes at once
});
GPU Acceleration Deep Dive:
The browser creates "composite layers" for GPU acceleration:
/* Creates composite layer (GPU accelerated) */
transform: translateX(100px); /* 3D transform */
will-change: transform; /* Hint: prepare layer */
/* Does NOT create composite layer (CPU) */
left: 100px; /* Positioned layout */
width: 200px; /* Layout property */
box-shadow: 0 0 10px black; /* Paint property */
When the browser renders a frame:
1. DOM → Render tree → Paint records → Composite layers
2. GPU processes composite layers
3. CPU handles paint operations
4. Browser combines output
Composite layers are expensive (memory), so using transform instead of left saves:
- CPU: Skip layout calculations
- Paint time: No need to raster pixels
- GPU: Let GPU handle transformation
Memory cost of layers:
// Each composite layer = memory allocation
// A 1920×1080 layer = ~8.3 MB (1920 × 1080 × 4 bytes RGBA)
// With will-change on 100 elements = 830 MB additional memory!
// Practical impact:
// - Desktop: Noticeable slowdown
// - Mobile: App crashes from OOM
RAF Synchronization with React:
React's rendering cycle doesn't naturally sync with RAF:
// Problem: React render happens at unpredictable times
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(c => c + 1); // Schedules React render
}, 16);
return () => clearInterval(interval);
}, []);
}
// Timeline:
// T0ms: interval fires → setCount queued
// T1ms: React starts render
// T3ms: React finishes render, commits to DOM
// T6ms: Browser paints
// T16ms: Next interval fires (while previous still painting!)
// Result: Queued updates, potential jank
// Better: Use RAF to sync with browser
function BetterAnimation() {
const [position, setPosition] = useState(0);
useEffect(() => {
let animationId;
const animate = () => {
setPosition(p => p + 1); // Still queues React, but timed correctly
animationId = requestAnimationFrame(animate);
};
animationId = requestAnimationFrame(animate);
return () => cancelAnimationFrame(animationId);
}, []);
}
// Best: Bypass React entirely for continuous animations
function OptimalAnimation() {
const posRef = useRef(0);
const elementRef = useRef(null);
useEffect(() => {
let animationId;
const animate = () => {
posRef.current += 1;
if (elementRef.current) {
elementRef.current.style.transform = `translateX(${posRef.current}px)`;
}
animationId = requestAnimationFrame(animate);
};
animationId = requestAnimationFrame(animate);
return () => cancelAnimationFrame(animationId);
}, []);
return <div ref={elementRef} />;
}
Event Handler Performance:
onMouseMove fires ~1000 times per second, making optimization critical:
// NAIVE: Direct state update
function DragBox() {
const [x, setX] = useState(0);
return (
<div
onMouseMove={(e) => setX(e.clientX)}
style={{ transform: `translateX(${x}px)` }}
>
Drag me
</div>
);
}
// Problem: 1000 state updates → 1000 React renders per second
// OPTIMIZED: RAF-throttled updates
function DragBoxOptimized() {
const [x, setX] = useState(0);
const isAnimating = useRef(false);
const handleMouseMove = (e) => {
if (!isAnimating.current) {
isAnimating.current = true;
requestAnimationFrame(() => {
setX(e.clientX);
isAnimating.current = false;
});
}
};
return (
<div
onMouseMove={handleMouseMove}
style={{ transform: `translateX(${x}px)` }}
>
Drag me
</div>
);
}
// Result: ~60 React renders per second (60 fps)
// BEST: Bypass React entirely
function DragBoxBest() {
const divRef = useRef(null);
const handleMouseMove = (e) => {
requestAnimationFrame(() => {
if (divRef.current) {
divRef.current.style.transform = `translateX(${e.clientX}px)`;
}
});
};
return <div ref={divRef} onMouseMove={handleMouseMove} />;
}
// Result: Zero React renders during drag
DevTools Measurement:
// Chrome DevTools Performance tab
performance.mark('animation-start');
// ... animate ...
performance.mark('animation-end');
performance.measure('animation', 'animation-start', 'animation-end');
const measure = performance.getEntriesByName('animation')[0];
console.log(`Animation took ${measure.duration}ms`);
console.log(`FPS: ${1000 / (measure.duration / 60)}`);
// Also check:
// - "Rendering" > "Rendering" tab shows GPU/CPU time
// - "Performance" > flame chart shows where time is spent
// - "Layers" tab shows composite layers created
</details>
<details> <summary><strong>🐛 Real-World Scenario: Product Showcase Page Optimization</strong></summary>
Scenario: Product showcase page with 50 cards that slide in on scroll. Client reports: "Scrolling is super janky, my phone lags for 5 seconds after scrolling."
Initial Investigation:
// Original code
function ProductCard({ position }) {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true); // Triggers animation via state
}
});
observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return (
<div
style={{
left: isVisible ? 0 : -100, // WRONG: Layout property
opacity: isVisible ? 1 : 0,
transition: 'all 0.6s ease-out'
}}
>
Product details...
</div>
);
}
Performance measurement:
Scrolling Performance Breakdown:
- Scroll event fires: 200ms
- Intersection Observer callback: 50ms (reasonable)
- setIsVisible: 0ms (instant queue)
- React render: 120ms (50 components re-render!)
- DOM commit: 20ms (update all 50 elements)
- Browser layout: 400ms (50 left position changes, each triggers layout)
- Paint: 150ms (repaint entire page)
- Composite: 80ms
- Total time: 820ms for one scroll frame
- Expected per frame: 16.67ms
- Actual frame rate: 1000 / 820 = 1.2 FPS 😱
After scrolling stops:
- Browser continues layout thrashing for 5 more frames: 5 × 820ms = 4100ms
Root causes identified:
leftproperty triggers layout - Should usetransform- 50 components re-render - Should use CSS animation or Intersection Observer alone
- Intersection Observer callback happens on every card - Cascading re-renders
Fix Implementation:
// SOLUTION 1: CSS animation without React
function ProductCardCSSOnly({ isInView }) {
return (
<div
className={isInView ? 'slide-in' : ''}
style={{
// No state-driven styles, only class-driven
}}
>
Product details...
</div>
);
}
/* CSS file */
.slide-in {
animation: slideIn 0.6s ease-out forwards;
}
@keyframes slideIn {
from {
transform: translateX(-100px); // GPU accelerated!
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
Measurement after fix:
Scrolling Performance Breakdown:
- Scroll event fires: 200ms
- Intersection Observer callback: 50ms
- setClassName: 0ms (queue)
- React render: 2ms (only className updates, no style recalc)
- DOM commit: 2ms (add class to 1-2 visible cards)
- CSS animation: 0ms (GPU handles transform)
- Browser layout: 0ms (no layout properties animated)
- Paint: 10ms (only animate opacity on new cards)
- Composite: 8ms
- Total time: 72ms per scroll event
- Frame rate: 1000 / 72 = 13.9 FPS (better but still chunky)
Further optimization using will-change:
function ProductCardOptimized({ isInView }) {
return (
<div
className={isInView ? 'slide-in' : 'slide-out'}
style={{
willChange: isInView ? 'transform' : 'auto' // Prepare for animation
}}
>
Product details...
</div>
);
}
// CSS
.slide-in {
animation: slideIn 0.6s ease-out forwards;
will-change: transform;
}
@keyframes slideIn {
from { transform: translateX(-100px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
Final measurement:
Scrolling Performance Breakdown:
- Scroll event: 200ms
- Observer callback: 50ms
- React render: 2ms
- DOM commit: 1ms
- GPU animation: 0ms (off-thread)
- Browser layout: 0ms
- Paint: 2ms
- Composite: 2ms
- Total: 57ms per scroll event
- Frame rate: ~17.5 FPS (smooth scrolling, animations on GPU)
Post-scroll performance:
- Animations complete: 600ms
- No continued layout thrashing
Before vs After:
// BEFORE: 1.2 FPS during scroll, 5s lag after
// Metrics: Layout: 400ms, Paint: 150ms, CPU: 100%
// AFTER: 17.5 FPS during scroll, smooth animations
// Metrics: Layout: 0ms, Paint: 2ms, GPU: 20%
</details>
<details> <summary><strong>⚖️ Trade-offs: Animation Strategies for Multiple Elements</strong></summary>
Animation Approach Comparison:
| Aspect | CSS Only | State-Driven | RAF + Ref | Framer Motion |
|---|---|---|---|---|
| Perf (60fps) | Yes (GPU) | 30-50fps | 60fps | 50-60fps |
| Responsive | No | Yes | Yes | Yes |
| Dev Time | Low | Low | Medium | Low |
| Bundle Size | 0KB | 0KB | 0KB | +42KB |
| Learning Curve | Low | Low | Medium | Medium |
| Complex Orchestration | Hard | Medium | Hard | Easy |
| Number Animations | Simple | Simple | Complex | Any |
CSS Animation Limitations:
// CSS can't respond to user input mid-animation
@keyframes slideIn {
from { transform: translateX(-100px); }
to { transform: translateX(0); }
}
// If user scrolls while animation playing, it continues to completion
// No way to interrupt or reverse based on interaction
State-Driven Animation Issues:
// React re-render overhead grows with component count
// 10 cards animating: 10 re-renders × 60fps = 600 renders/second
// 100 cards: 6,000 renders/second (browser can't keep up)
// Solution: Use CSS for many elements, state for a few
RAF + Ref Approach Trade-offs:
// Advantages:
// - Zero React re-renders during animation
// - Full control over animation logic
// - Can respond to user input in real-time
// Disadvantages:
// - More code to write
// - Manual cleanup required (cancelAnimationFrame)
// - Harder to orchestrate multiple animations
// - Difficult to integrate with React data
// Best for: Dragging, continuous feedback (e.g., game, data viz)
</details>
<details> <summary><strong>💬 Explain to Junior: Performance Debugging Simplified</strong></summary>
Choosing Animation Strategy by Use Case:
// Button hover: CSS only
.btn:hover { transform: scale(1.05); transition: transform 0.2s; }
// Form submission feedback: State + CSS
function Form() {
const [submitted, setSubmitted] = useState(false);
return (
<div className={submitted ? 'submitted' : ''}>
{/* CSS handles animation on class change */}
</div>
);
}
// Drag interaction: RAF + Ref
function DraggableBox() {
const ref = useRef(null);
useEffect(() => {
document.addEventListener('mousemove', (e) => {
ref.current.style.transform = `translate(${e.clientX}px, ${e.clientY}px)`;
});
}, []);
return <div ref={ref} />;
}
// Complex orchestration (list animations): Framer Motion
<motion.ul variants={containerVariants}>
{items.map(item => (
<motion.li variants={itemVariants} key={item.id}>{item.name}</motion.li>
))}
</motion.ul>
💬 Explain to Junior
Performance Problem Explanation:
Imagine you're directing an orchestra. Each musician is a browser process:
- Layout violinist: Calculates where everything sits
- Paint violinist: Colors in the pixels
- Composite violinist: Combines layers for display
When you animate a left property:
You: "Violinist, move 3 pixels left"
Layout violinist: "I need to recalculate EVERYTHING to know where to move!"
(checks if other elements shift, cascading calculations)
Paint violinist: "I need to repaint everything I moved!"
Composite violinist: "I need to rebuild the final image!"
Result: All three play their instruments at once (chaotic!)
When you animate transform:
You: "GPU, please shift the pixels 3 positions left"
GPU: "Sure, I'll do that off-thread without bothering anyone"
Layout violinist: *keeps playing normally*
Paint violinist: *keeps painting normally*
Composite violinist: *layers are ready*
Result: Smooth, parallel processing!
Real-World Performance Debugging:
You're a performance detective. When animations are janky:
-
Check: What property is animating?
- If it's
left,top,width,height→ Layout thrashing (fix: usetransform) - If it's
opacity,transform→ Probably OK
- If it's
-
Check: How many elements?
- < 10 elements: State-driven is fine
- 10-50 elements: Use CSS + intersection observer
- 50+ elements: CSS-only or virtualization
-
Check: Is React re-rendering?
- Open DevTools, React Profiler
- Look for re-renders during animation
- If yes: Use refs to bypass React
-
Check: GPU layers
- DevTools > Layers tab
- Are composite layers being created?
- Too many? Remove
will-changefrom non-animating elements
Interview Answer Template:
"Performance in React animations comes down to several key considerations:
First, which properties you animate matters tremendously. The browser has a critical rendering path: JavaScript, Style calculation, Layout, Paint, Composite. If you animate transform or opacity, you skip Layout and Paint—the GPU handles it. If you animate width or left, you force the entire rendering pipeline to repeat, causing jank. At 60fps, that's expensive.
Second, minimize React re-renders. Every setState triggers a render. For continuous animations, don't update state—use a ref and update the DOM directly. This is especially important with many elements. For example, if you have 50 animated cards, state-driven means 50 re-renders × 60fps = 3,000 renders per second. Your browser can't keep up.
Third, be careful with will-change. It tells the browser to prepare for animation by creating a composite layer, which costs memory. One will-change element? Fine. A hundred? You've eaten several megabytes. On mobile, that's a crash.
Fourth, avoid layout thrashing. When you read then write DOM properties (like offsetHeight then style.left), the browser must flush layout calculations repeatedly. Batch reads together, then writes together.
In practice: Use CSS for simple animations, use state with CSS animations for react-driven triggers, use refs for continuous interactions, use Framer Motion for complex orchestration. Always measure with DevTools Performance tab—looks can be deceiving."
Common Pitfalls:
- Animating the wrong property
// WRONG
<div style={{ width: isAnimating ? '500px' : '0px' }} />
// RIGHT
<div style={{ transform: isAnimating ? 'scaleX(2.5)' : 'scaleX(0)' }} />
- Too many
will-changedeclarations
// WRONG: Will-change on everything
* { will-change: all; }
// RIGHT: Only on animated elements
.animated { will-change: transform, opacity; }
- Not cleaning up RAF
// WRONG: Memory leak
useEffect(() => {
requestAnimationFrame(() => {
// update...
});
});
// RIGHT: Cleanup
useEffect(() => {
let id = requestAnimationFrame(() => {});
return () => cancelAnimationFrame(id);
}, []);
</details>
Source: animations-01-techniques.md