Once you’ve mastered the fundamentals of React animation performance, these advanced techniques will help you create sophisticated, high-performance animations that scale to complex applications. These patterns are essential for applications with heavy animation requirements or performance-critical interactions.
Advanced Animation Techniques
Intersection Observer for Performance
// Performance-conscious animation with Intersection Observer
function useInViewAnimation(threshold = 0.1) {
const [isVisible, setIsVisible] = useState(false);
const elementRef = useRef<HTMLElement>(null);
useEffect(() => {
const element = elementRef.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
// Stop observing once animation is triggered
observer.disconnect();
}
},
{
threshold,
// Add margin to trigger animation before element is fully visible
rootMargin: '50px 0px',
},
);
observer.observe(element);
return () => observer.disconnect();
}, [threshold]);
return { isVisible, elementRef };
}
// Animated component that only animates when in view
function LazyAnimatedCard({ children }: { children: React.ReactNode }) {
const { isVisible, elementRef } = useInViewAnimation();
return (
<div ref={elementRef} className={`animated-card ${isVisible ? 'animated-card--visible' : ''}`}>
{children}
</div>
);
}
// CSS for the lazy animation
const lazyAnimationStyles = `
.animated-card {
opacity: 0;
transform: translateY(50px);
transition: opacity 0.6s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
will-change: opacity, transform;
}
.animated-card--visible {
opacity: 1;
transform: translateY(0);
}
/* Remove will-change after animation completes */
.animated-card--visible {
animation: removeWillChange 0.6s forwards;
}
@keyframes removeWillChange {
to {
will-change: auto;
}
}
`;Web Animations API Integration
// High-performance animations with Web Animations API
function useWebAnimation() {
const elementRef = useRef<HTMLElement>(null);
const animate = useCallback((keyframes: Keyframe[], options: KeyframeAnimationOptions = {}) => {
if (!elementRef.current) return;
const animation = elementRef.current.animate(keyframes, {
duration: 300,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
fill: 'forwards',
...options,
});
return animation;
}, []);
const fadeIn = useCallback(() => {
return animate([
{ opacity: 0, transform: 'scale(0.9)' },
{ opacity: 1, transform: 'scale(1)' },
]);
}, [animate]);
const slideUp = useCallback(() => {
return animate([{ transform: 'translateY(100%)' }, { transform: 'translateY(0)' }]);
}, [animate]);
const pulse = useCallback(() => {
return animate(
[{ transform: 'scale(1)' }, { transform: 'scale(1.05)' }, { transform: 'scale(1)' }],
{
duration: 600,
iterations: 3,
},
);
}, [animate]);
return {
elementRef,
animate,
fadeIn,
slideUp,
pulse,
};
}
// Component using Web Animations API
function WebAnimatedButton({
children,
onClick,
}: {
children: React.ReactNode;
onClick: () => void;
}) {
const { elementRef, pulse, fadeIn } = useWebAnimation();
const [isLoading, setIsLoading] = useState(false);
const handleClick = async () => {
if (isLoading) return;
setIsLoading(true);
// Play pulse animation
const animation = pulse();
if (animation) {
// Wait for animation to complete
await animation.finished;
}
onClick();
setIsLoading(false);
};
useEffect(() => {
// Fade in on mount
fadeIn();
}, [fadeIn]);
return (
<button
ref={elementRef}
onClick={handleClick}
disabled={isLoading}
className="web-animated-button"
>
{isLoading ? 'Loading...' : children}
</button>
);
}Testing Animation Performance
// Animation performance testing utilities
class AnimationTester {
async testAnimationPerformance(
component: React.ComponentType<any>,
props: any,
animationTrigger: () => void,
duration: number = 1000,
): Promise<{
averageFPS: number;
frameDrops: number;
grade: string;
recommendations: string[];
}> {
const container = document.createElement('div');
document.body.appendChild(container);
// Setup performance monitoring
const monitor = new AnimationPerformanceMonitor();
const frameRates: number[] = [];
let frameDrops = 0;
monitor.startMonitoring((fps) => {
frameRates.push(fps);
if (fps < 30) frameDrops++;
});
try {
// Render component
const root = ReactDOM.createRoot(container);
root.render(React.createElement(component, props));
// Wait for initial render
await new Promise((resolve) => setTimeout(resolve, 100));
// Trigger animation
animationTrigger();
// Monitor for specified duration
await new Promise((resolve) => setTimeout(resolve, duration));
monitor.stopMonitoring();
const averageFPS = frameRates.reduce((sum, fps) => sum + fps, 0) / frameRates.length;
const grade = monitor.getPerformanceGrade();
const recommendations = this.generateRecommendations(averageFPS, frameDrops);
return {
averageFPS,
frameDrops,
grade,
recommendations,
};
} finally {
document.body.removeChild(container);
}
}
private generateRecommendations(averageFPS: number, frameDrops: number): string[] {
const recommendations: string[] = [];
if (averageFPS < 45) {
recommendations.push('Consider using transform/opacity instead of layout properties');
recommendations.push('Reduce animation complexity or duration');
}
if (frameDrops > 10) {
recommendations.push(
'Too many dropped frames - check for expensive operations during animation',
);
}
if (averageFPS < 30) {
recommendations.push('Critical performance issue - animation may be blocking main thread');
}
return recommendations;
}
}
// Jest integration for animation testing
describe('Animation Performance', () => {
const tester = new AnimationTester();
it('should maintain 60fps during modal animation', async () => {
const results = await tester.testAnimationPerformance(
PerformantModal,
{ isOpen: false, onClose: () => {} },
() => {
// Trigger modal open
const button = document.querySelector('[data-testid="open-modal"]');
button?.click();
},
500, // 500ms animation
);
expect(results.averageFPS).toBeGreaterThan(45);
expect(results.frameDrops).toBeLessThan(5);
expect(results.grade).not.toBe('F');
});
});