Framer Motion文本动画与GSAP自定义光标动画性能干扰问题排查及修复方案咨询
Framer Motion文本动画与GSAP自定义光标动画性能干扰问题排查及修复方案咨询
嘿,我之前也碰到过类似的动画冲突问题,结合你给出的代码和技术栈,咱们来一步步分析问题出在哪,以及怎么修复它~
问题原因分析
这两个动画互相干扰主要有以下几个关键点:
- 高频事件的动画实例堆积:
mousemove事件每秒会触发几十次,你当前的代码每次触发都会用GSAP创建一个新的to动画实例,这些实例会抢占浏览器的动画帧资源,导致Framer Motion的文本动画得不到足够的帧时间,出现卡顿。 - 定时器精度被干扰:你的
AnimatedText组件用了setTimeout来调度下一次动画,而setTimeout的执行依赖浏览器的事件循环。当mousemove事件频繁触发时,事件队列被占满,setTimeout的回调会被延迟执行,直接打乱了文本动画的时序。 - 动画引擎的帧调度冲突:Framer Motion和GSAP都基于
requestAnimationFrame实现动画,但如果GSAP的光标动画频繁抢占帧,会导致Framer Motion的文本动画帧被挤压,出现视觉上的停顿。
具体修复方案
下面是几个可以单独或组合使用的修复方法,亲测有效:
方案一:优化GSAP光标动画的触发逻辑,避免重复创建动画
不要每次mousemove都创建新动画,而是先记录鼠标位置,再用GSAP的ticker统一驱动动画,同时开启overwrite: auto避免动画实例堆积:
import { useEffect, useRef } from 'react'; import { gsap } from 'gsap'; export const CustomCursor = () => { const cursorRef = useRef(null); const mousePosRef = useRef({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); useEffect(() => { // 初始化光标位置 gsap.set(cursorRef.current, { x: mousePosRef.current.x, y: mousePosRef.current.y }); // 只更新鼠标位置,不直接创建动画 const handleMouseMove = (e) => { mousePosRef.current.x = e.clientX; mousePosRef.current.y = e.clientY; }; window.addEventListener('mousemove', handleMouseMove); // 用GSAP的ticker统一驱动动画,复用实例 const ticker = gsap.ticker.add(() => { gsap.to(cursorRef.current, { x: mousePosRef.current.x, y: mousePosRef.current.y, duration: 0.3, ease: "power2.out", overwrite: "auto" // 自动覆盖未完成的动画,避免堆积 }); }); // 清理监听和ticker return () => { window.removeEventListener('mousemove', handleMouseMove); gsap.ticker.remove(ticker); }; }, []); return ( <div ref={cursorRef} style={{ position: 'fixed', width: '20px', height: '20px', backgroundColor: '#000', borderRadius: '50%', pointerEvents: 'none', zIndex: 9999, willChange: 'transform' // 告诉浏览器提前优化渲染层 }} /> ); };
方案二:替换setTimeout为更可靠的时序控制
setTimeout的精度不足,建议改用更严谨的定时器管理,让文本动画的调度和动画引擎同步:
"use client"; import React, { useCallback, useEffect, useState, useRef } 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 timerRef = useRef(null); const startAnimation = useCallback(() => { const nextItem = items[items.indexOf(currentItem) + 1] || items[0]; setCurrentItem(nextItem); setIsAnimating(true); }, [currentItem, items]); useEffect(() => { const scheduleNext = () => { timerRef.current = setTimeout(() => { startAnimation(); }, duration); }; if (!isAnimating) scheduleNext(); // 组件卸载或依赖变化时清理定时器 return () => { if (timerRef.current) clearTimeout(timerRef.current); }; }, [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> ); };
方案三:启用GSAP的自动休眠功能
GSAP默认有autoSleep功能,闲置一段时间后会暂停动画引擎,减少资源占用,你可以在初始化时开启:
useEffect(() => { // 闲置1秒后自动休眠动画引擎 gsap.config({ autoSleep: 1000 }); // ... 其他光标动画代码 }, []);
总结
优先尝试方案一优化光标动画的触发逻辑,这是解决冲突最关键的一步;如果还有轻微卡顿,再结合方案二和三调整文本动画的时序和GSAP的配置,基本就能让两个动画独立运行互不干扰了。
内容来源于stack exchange
相关产品推荐
相关产品推荐

