You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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元素,路由变化时先执行旧页面退场动画,完成后移除旧元素并渲染新页面,最后执行新页面入场动画:

  1. 在CustomPageTransition中维护两个ref,分别保存当前页面和上一个页面的容器元素。
  2. 监听routeChangeStart事件,触发旧页面退场动画;动画完成后切换ref指向,渲染新页面并触发入场动画。

方案2:CSS类+GSAP结合控制

通过CSS类标记页面的过渡状态,用GSAP控制类对应的样式变化:

  1. 给页面容器定义page-exit、page-enter类,包含退场和入场的初始样式。
  2. 路由变化时,先给当前页面添加page-exit类,执行GSAP退场动画;动画完成后切换路由,给新页面添加page-enter类并执行入场动画。
  3. 用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 11:05:21