reactjsperformanceanimationgsapframer-motion

React Animation Component Pauses When Custom Cursor Moves - Performance Interference Issue


I'm experiencing a performance issue where a React animation component stops working properly when a custom cursor moves around the page. The two animations seem to interfere with each other.

I have two separate animation systems running simultaneously:

  1. Text Animation Component: Uses Framer Motion for animated text transitions
  2. Custom Cursor: Uses GSAP for smooth mouse following

When the cursor moves, the text animation appears to pause or stutter, disrupting the timing cycle.

Text Animation Component

"use client";
import React, { useCallback, useEffect, useState } from "react";
import { AnimatePresence, motion } from "motion/react";

export const AnimatedText = ({ items, duration = 3000 }) => {
  const [currentItem, setCurrentItem] = useState(items[0]);
  const [isAnimating, setIsAnimating] = useState(false);

  const startAnimation = useCallback(() => {
    const nextItem = items[items.indexOf(currentItem) + 1] || items[0];
    setCurrentItem(nextItem);
    setIsAnimating(true);
  }, [currentItem, items]);

  useEffect(() => {
    if (!isAnimating) {
      setTimeout(() => {
        startAnimation();
      }, duration);
    }
  }, [isAnimating, duration, startAnimation]);

  return (
    <AnimatePresence onExitComplete={() => setIsAnimating(false)}>
      <motion.div
        initial={{ opacity: 0, y: 10 }}
        animate={{ opacity: 1, y: 0 }}
        exit={{ opacity: 0, y: -40, x: 40, filter: "blur(8px)", scale: 2 }}
        transition={{ type: "spring", stiffness: 100, damping: 10 }}
        key={currentItem}
      >
        {currentItem.split(" ").map((word, wordIndex) => (
          <motion.span
            key={word + wordIndex}
            initial={{ opacity: 0, y: 10, filter: "blur(8px)" }}
            animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
            transition={{ delay: wordIndex * 0.3, duration: 0.3 }}
          >
            {word.split("").map((letter, letterIndex) => (
              <motion.span
                key={word + letterIndex}
                initial={{ opacity: 0, y: 10, filter: "blur(8px)" }}
                animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
                transition={{
                  delay: wordIndex * 0.3 + letterIndex * 0.05,
                  duration: 0.2,
                }}
              >
                {letter}
              </motion.span>
            ))}
          </motion.span>
        ))}
      </motion.div>
    </AnimatePresence>
  );
};

Custom Cursor

import { useEffect, useRef } from 'react';
import { gsap } from 'gsap';

export const CustomCursor = () => {
  const cursorRef = useRef(null);

  useEffect(() => {
    const handleMouseMove = (e) => {
      gsap.to(cursorRef.current, {
        x: e.clientX,
        y: e.clientY,
        duration: 0.3,
        ease: "power2.out"
      });
    };

    document.addEventListener('mousemove', handleMouseMove);
    return () => document.removeEventListener('mousemove', handleMouseMove);
  }, []);

  return (
    <div
      ref={cursorRef}
      style={{
        position: 'fixed',
        width: '20px',
        height: '20px',
        backgroundColor: '#000',
        borderRadius: '50%',
        pointerEvents: 'none',
        zIndex: 9999,
      }}
    />
  );
};

Expected Behavior: Both animations should run independently without interfering with each other.

Actual Behavior: The text animation timing gets disrupted when the cursor moves, causing pauses or stutters in the animation cycle.

Environment: React 18, Next.js 14, Framer Motion 11+, GSAP 3.12+, TypeScript.

  1. What causes this interference between Framer Motion and GSAP animations?
  2. How to fix this issue?

Solution

  • When using GSAP inside React's useEffect, it's recommended to wrap your animations inside a GSAP context to scope them properly and clean up automatically, preventing memory leaks. Alternatively, you can use a ready-made hook like useGsap from @gsap/react which handles this pattern for you.
    https://gsap.com/docs/v3/GSAP/gsap.context()/
    https://gsap.com/resources/React/

    For mouse movement animations with GSAP, it's best to use either quickSetter or quickTo:

    Use quickTo if you want smooth movement with easing
    https://gsap.com/docs/v3/GSAP/gsap.quickTo()/

    For maximum performance, use quickSetter. It updates the value immediately without animation, perfect for fast, direct updates like tracking the cursor position.
    https://gsap.com/docs/v3/GSAP/gsap.quickSetter()

    example using quickTo with useGsap hook

    import { useRef } from 'react';
    import { gsap } from 'gsap';
    import { useGsap } from '@gsap/react';
    
    export const CustomCursor = () => {
      const cursorRef = useRef<HTMLDivElement>(null);
    
      useGsap(() => {
        if (!cursorRef.current) return;
    
        const moveX = gsap.quickTo(cursorRef.current, 'x', {
          duration: 0.3,
          ease: 'power2.out',
        });
    
        const moveY = gsap.quickTo(cursorRef.current, 'y', {
          duration: 0.3,
          ease: 'power2.out',
        });
    
        const handleMouseMove = (e: MouseEvent) => {
          moveX(e.clientX);
          moveY(e.clientY);
        };
    
        document.addEventListener('mousemove', handleMouseMove);
        return () => document.removeEventListener('mousemove', handleMouseMove);
      }, { scope: cursorRef });
    
      return (
        <div
          ref={cursorRef}
          style={{
            position: 'fixed',
            width: 20,
            height: 20,
            backgroundColor: '#000',
            borderRadius: '50%',
            pointerEvents: 'none',
            zIndex: 9999,
            transform: 'translate(-50%, -50%)',
            willChange: 'transform',
          }}
        />
      );
    };