Steve Kinney

Advanced Animation Performance Techniques

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');
  });
});

Last modified on .