Next.js中用useMemo实现页面过渡时组件重跑问题及方案咨询
Next.js页面过渡动画问题排查与替代方案
问题背景
在Next.js中使用GSAP实现页面过渡效果,项目限制仅能使用GSAP库。当前实现逻辑是通过useMemo追踪子组件,待退场过渡动画完成后更新显示的子组件,再触发新页面的入场效果。但仅在build生产模式下,页面卸载(退场动画执行期间)时部分组件会出现重新运行的情况,开发模式无此问题。
相关代码
_app.tsx
function MainApp({ Component, pageProps }: AppProps) { return ( <CustomPageTransition> <Component {...pageProps} key={router.asPath} /> </CustomPageTransition> ) }
CustomPageTransition.tsx
export const CustomPageTransition = function ({ children }: { children: ReactNode}) { const [displayChildren, setDisplayChildren] = useState(children) const memoizedChildren = useMemo(() => children, [children]) const [readyToRenderNextPage, setReadyToRenderNextPage] = useState(false) // startCover gsap animation const startCover = useCallback(() => { const coverTl = gsap.timeline() if (el.current) { coverTl.to(el.current, { opacity: 1, ease: 'Power2.inOut', duration: 0.3, onComplete: () => { setTimeout(() => { setReadyToRenderNextPage(true) }, 300) }, }) } }, [el]) // outro page transition after first load useEffect(() => { // on exit page router.events.on('routeChangeStart', startCover) return () => { router.events.off('routeChangeStart', startCover) } }, [router.events, startCover]) // intro page transition after first load useEffect(() => { if (isIntroPageTransitionAnimating) { // true when page component mounted, this is updated via Context startReveal() } }, [isIntroPageTransitionAnimating, startReveal]) useIsomorphicLayoutEffect(() => { // outro: switch to new children when readyToRenderNextPage if (memoizedChildren !== displayChildren && readyToRenderNextPage) { setDisplayChildren(children) } }, [memoizedChildren, displayChildren, readyToRenderNextPage]) return ( <> <StyledPageTransition ref={el} className={pageTransitionClass} /> {displayChildren} </> ) }
问题排查方向
- React Strict Mode影响:Next.js生产模式默认开启Strict Mode,会双重调用组件挂载相关逻辑(如
useEffect、useMemo),可能导致卸载阶段组件重复执行。可临时关闭Strict Mode验证是否为此原因。 - useIsomorphicLayoutEffect执行时机:生产环境中LayoutEffect的执行时机与开发模式存在差异,可能导致子组件更新时机异常,尝试替换为普通
useEffect测试。 - 路由事件监听异常:检查
router.events.on/off在生产环境是否存在解绑不彻底的情况,导致多次触发startCover,提前更新readyToRenderNextPage,让新页面过早渲染。 - React元素引用比较逻辑:生产环境打包优化(如Tree Shaking、代码压缩)可能导致React元素引用变化,使得
memoizedChildren !== displayChildren的判断出错,提前触发子组件更新。 - setTimeout延迟不匹配:生产环境代码执行速度更快,300ms固定延迟可能与GSAP动画实际完成时间不匹配,导致新页面在旧页面退场未完成时开始渲染,引发组件重复运行。
替代实现方案
方案1:基于DOM引用的直接过渡控制
用useRef存储当前和上一个页面的DOM元素,路由变化时先执行旧页面退场动画,完成后移除旧元素并渲染新页面,最后执行新页面入场动画:
- 在
CustomPageTransition中维护两个ref,分别保存当前页面和上一个页面的容器元素。 - 监听
routeChangeStart事件,触发旧页面退场动画;动画完成后切换ref指向,渲染新页面并触发入场动画。
方案2:CSS类+GSAP结合控制
通过CSS类标记页面的过渡状态,用GSAP控制类对应的样式变化:
- 给页面容器定义
page-exit、page-enter类,包含退场和入场的初始样式。 - 路由变化时,先给当前页面添加
page-exit类,执行GSAP退场动画;动画完成后切换路由,给新页面添加page-enter类并执行入场动画。 - 用
useEffect监听路由状态变化,同步控制类名切换和动画触发。
方案3:优化现有逻辑
- 移除
memoizedChildren的引用比较逻辑,直接通过路由事件的回调控制子组件切换时机,避免React元素引用不稳定的问题。 - 将
setTimeout替换为GSAP timeline的链式调用,确保动画时序精准:coverTl.to(el.current, { opacity: 1, ease: 'Power2.inOut', duration: 0.3, }).add(() => { setReadyToRenderNextPage(true) }, "+=0.3")
内容的提问来源于stack exchange,提问作者havingaheadache
相关产品推荐
相关产品推荐

