React.memo结合Framer Motion与Next.js Image组件的问题解决
问题解决方案
1. 修复hero为null时的图片渲染异常
next/image的src属性不允许为空字符串,当hero为null时传递空src会触发渲染错误;同时仅通过visibility: hidden隐藏图片仍会触发Image组件的加载逻辑,造成资源浪费。修改方案如下:
在HeroImage组件中,仅当hero存在时才渲染Image组件,否则返回空占位容器:
const HeroImage = React.memo( ({ hero, index }: { hero: Hero | null; index: number }) => { if (!hero) return <div className="relative w-full h-full" />; return ( <motion.div className="relative w-full h-full" initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: 20 }} transition={{ duration: 0.3 }} > <Image src={hero.img} alt={hero.name} layout="fill" objectFit="cover" draggable={false} /> </motion.div> ); }, // 自定义memo比较逻辑,避免对象引用变化导致的无效重渲染 (prevProps, nextProps) => { if (prevProps.hero === nextProps.hero) return true; return prevProps.hero?.name === nextProps.hero?.name && prevProps.hero?.img === nextProps.hero?.img; } );
2. 解决切换英雄时所有图片触发动画的问题
核心原因
- 默认
React.memo的浅比较无法准确判断hero对象的内容变化,导致不必要的组件重渲染 framer-motion的initial动画默认在组件每次更新时触发,而非仅当hero从null变为有效数据时
修改方案
- 自定义React.memo比较函数:精确对比
hero的name和img属性,避免因对象引用变化触发重渲染 - 控制动画触发时机:通过状态跟踪
hero的变化,仅当hero从null变为有效数据时触发入场动画
完整修改后的代码:
import { motion } from "framer-motion"; import Image from "next/image"; import React, { useEffect, useState } from "react"; interface Hero { name: string; img: string; } const HeroImage = React.memo( ({ hero, index }: { hero: Hero | null; index: number }) => { const [shouldAnimate, setShouldAnimate] = useState(false); useEffect(() => { // 仅当hero从null变为有效数据时触发动画 if (hero) { setShouldAnimate(true); } else { setShouldAnimate(false); } }, [hero]); if (!hero) return <div className="relative w-full h-full" />; return ( <motion.div className="relative w-full h-full" initial={{ opacity: 0, x: -20 }} animate={shouldAnimate ? { opacity: 1, x: 0 } : { opacity: 1, x: 0 }} exit={{ opacity: 0, x: 20 }} transition={{ duration: 0.3 }} onExitComplete={() => setShouldAnimate(false)} > <Image src={hero.img} alt={hero.name} layout="fill" objectFit="cover" draggable={false} /> </motion.div> ); }, (prevProps, nextProps) => { if (prevProps.hero === nextProps.hero) return true; if (!prevProps.hero && !nextProps.hero) return true; return prevProps.hero?.name === nextProps.hero?.name && prevProps.hero?.img === nextProps.hero?.img; } ); const HeroCard = React.memo( ({ hero, index }: { hero: Hero | null; index: number }) => { return ( <div className="relative overflow-hidden rounded flex-grow bg-cover" style={{ backgroundImage: `url('/placeholder.png')`, }} > <HeroImage hero={hero} index={index} /> <div className="absolute bottom-0 left-0 right-0 bg-black/70 text-white text-center py-1"> {hero?.name || "未选择英雄"} </div> </div> ); }, (prevProps, nextProps) => { if (prevProps.hero === nextProps.hero) return true; if (!prevProps.hero && !nextProps.hero) return true; return prevProps.hero?.name === nextProps.hero?.name && prevProps.hero?.img === nextProps.hero?.img; } ); export { HeroImage, HeroCard };
额外优化建议
- 父组件渲染
HeroCard列表时,使用hero?.name或唯一ID作为key(而非index),避免列表重排引发的无效渲染:// 父组件示例 {heroList.map((hero, idx) => ( <HeroCard key={hero?.name || `hero-card-${idx}`} hero={hero} index={idx} /> ))} - 移除
layoutId属性(除非需要跨组件布局动画),该属性会让framer-motion额外跟踪元素布局,可能引发不必要的重渲染
内容的提问来源于stack exchange,提问作者Rahul Sahani
相关产品推荐
相关产品推荐

