Skip to content

Framer Motion性能优化与最佳实践 ​

性能优化策略 ​

1. 使用useReducedMotion适配用户偏好 ​

某些用户可能在系统设置中开启了减少动画选项,我们应该尊重这个设置:

tsx
import { useReducedMotion } from "framer-motion"

function AdaptiveAnimation() {
  const shouldReduceMotion = useReducedMotion()
  
  const variants = {
    hidden: { opacity: 0 },
    visible: {
      opacity: 1,
      x: shouldReduceMotion ? 0 : 100,
      transition: {
        duration: shouldReduceMotion ? 0.1 : 0.5
      }
    }
  }

  return (
    <motion.div
      variants={variants}
      initial="hidden"
      animate="visible"
    >
      适应性动画
    </motion.div>
  )
}

2. 使用useMemo优化动画配置 ​

对于复杂的动画配置,应该使用useMemo避免不必要的重新计算:

tsx
function OptimizedAnimation() {
  const [isHovered, setIsHovered] = useState(false);
  
  const variants = useMemo(() => ({
    idle: {
      scale: 1,
      boxShadow: "0px 0px 0px rgba(0,0,0,0.2)"
    },
    hover: {
      scale: 1.05,
      boxShadow: "0px 5px 15px rgba(0,0,0,0.2)"
    }
  }), []); // 配置不依赖于任何状态,只需计算一次

  return (
    <motion.div
      variants={variants}
      animate={isHovered ? "hover" : "idle"}
      onHoverStart={() => setIsHovered(true)}
      onHoverEnd={() => setIsHovered(false)}
    >
      优化后的动画
    </motion.div>
  )
}

3. 使用layout属性时的性能优化 ​

tsx
function OptimizedLayout() {
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <div style={{ padding: 20 }}>
      <motion.div
        layout
        // 使用layoutId可以避免不必要的布局计算
        layoutId="expandable-card"
        // 使用style而不是animate来设置非动画属性
        style={{
          width: isExpanded ? 300 : 150,
          height: isExpanded ? 300 : 150,
          background: '#ea4335',
        }}
        // 只对真正需要动画的属性使用transition
        transition={{
          layout: {
            duration: 0.3,
            ease: "easeInOut"
          }
        }}
        onClick={() => setIsExpanded(!isExpanded)}
      />
    </div>
  )
}

动画编排 ​

1. 使用useAnimation控制复杂动画序列 ​

tsx
function SequenceAnimation() {
  const controls = useAnimation();
  
  async function sequence() {
    await controls.start({ scale: 1.5 });
    await controls.start({ rotate: 180 });
    await controls.start({ scale: 1 });
    return await controls.start({ rotate: 0 });
  }

  return (
    <div>
      <motion.div
        animate={controls}
        initial={{ scale: 1, rotate: 0 }}
        style={{
          width: 100,
          height: 100,
          background: '#4285f4',
          borderRadius: 10
        }}
      />
      <button onClick={sequence}>
        播放动画序列
      </button>
    </div>
  )
}

2. 使用useAnimationControls实现更细粒度的控制 ​

tsx
function ControlledAnimation() {
  const controls = useAnimationControls();
  const [status, setStatus] = useState('idle');

  const startAnimation = async () => {
    setStatus('animating');
    await controls.start({
      scale: [1, 2, 2, 1],
      rotate: [0, 0, 180, 180],
      borderRadius: ["10%", "50%", "50%", "10%"],
    });
    setStatus('completed');
  };

  return (
    <div>
      <motion.div
        animate={controls}
        initial={{ scale: 1 }}
        style={{
          width: 100,
          height: 100,
          background: '#fbbc05',
        }}
      />
      <button 
        onClick={startAnimation}
        disabled={status === 'animating'}
      >
        {status === 'animating' ? '动画中...' : '开始动画'}
      </button>
    </div>
  )
}

常见陷阱与解决方案 ​

1. 避免动画过程中的布局抖动 ​

tsx
function StableAnimation() {
  return (
    <motion.div
      style={{
        // 使用transform而不是width/height来实现缩放
        width: 100,
        height: 100,
        background: '#34a853',
      }}
      whileHover={{
        scale: 1.1,  // 使用scale而不是width/height
        // 避免使用margin/padding的动画
      }}
    />
  )
}

2. 处理动画中的内容溢出 ​

tsx
function OverflowHandling() {
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <div style={{ overflow: 'hidden' }}> {/* 父容器处理溢出 */}
      <motion.div
        initial={false}
        animate={{
          height: isExpanded ? 'auto' : 0
        }}
        transition={{
          duration: 0.3,
          ease: "easeInOut"
        }}
      >
        <div style={{ padding: 20 }}>
          展开/收起的内容
        </div>
      </motion.div>
    </div>
  )
}

实战案例:优化的图片加载动画 ​

tsx
function OptimizedImageLoader() {
  const [isLoaded, setIsLoaded] = useState(false);
  const controls = useAnimationControls();

  const handleLoad = async () => {
    setIsLoaded(true);
    // 使用序列动画使加载效果更流畅
    await controls.start({ opacity: 1 });
    await controls.start({ scale: 1 });
  };

  return (
    <div style={{ overflow: 'hidden' }}>
      <motion.img
        src="large-image.jpg"
        initial={{ opacity: 0, scale: 0.8 }}
        animate={controls}
        onLoad={handleLoad}
        style={{
          width: '100%',
          height: 'auto',
          willChange: 'transform', // 优化性能
        }}
        transition={{
          opacity: { duration: 0.3 },
          scale: { duration: 0.3, delay: 0.1 }
        }}
      />
      {!isLoaded && (
        <motion.div
          initial={{ opacity: 0.5 }}
          animate={{ opacity: 1 }}
          transition={{
            repeat: Infinity,
            duration: 1,
            ease: "easeInOut"
          }}
        >
          加载中...
        </motion.div>
      )}
    </div>
  )
}

调试技巧 ​

1. 使用开发者工具 ​

tsx
import { motion, MotionConfig } from "framer-motion"

function DebugComponent() {
  return (
    <MotionConfig reducedMotion="user">
      <motion.div
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        transition={{ duration: 0.5 }}
        // 开发环境下显示动画信息
        whileHover={{ scale: 1.1 }}
      />
    </MotionConfig>
  )
}

小结 ​

本文介绍了Framer Motion的性能优化策略和最佳实践:

  • 性能优化技巧
  • 动画编排方法
  • 常见问题解决方案
  • 调试技巧

相关资源 ​