Next.js路由跳转全应用重渲染 疑与next-auth SessionProvider相关
问题描述
环境信息
- Next.js 版本:
12.1.6 - react-dom 版本:
18.0.5 - next-auth 版本:
4.6.1
异常现象
应用当前仅包含首页、登录页两个路由,在两个路由间通过Next.js Link组件互相跳转时,整个应用会发生非预期重渲染。
通过react-profiler检测到重渲染触发原因为context变更,初步怀疑问题和next-auth提供的SessionProvider有关,但无法确定全应用范围重渲染的触发逻辑。
从检测结果可见,自定义的HeaderComponent并未发生重渲染,原因是对该组件使用了React.memo做渲染缓存,但不希望为所有组件都手动包裹React.memo。该HeaderComponent是在pages/_app.tsx内的全局Layout组件中引入的。
相关代码
pages/_app.tsx 实现:
function App({ Component, pageProps }: AppProps) { return ( <> <Head> <title>foo</title> <meta name="description" content="Generated by create next app" /> <link rel="icon" href="/favicon.ico" /> </Head> <SessionProvider session={ pageProps.session }> <LayoutComponent> <Component {...pageProps} /> </LayoutComponent> </SessionProvider> </> ); }
LayoutComponent 实现:
const LayoutComponent = ({ children }: PropsWithChildren) => { return ( <> <HeaderComponent homePath='/' signinPath='/auth/signin' aboutPath='/about' /> <main> { children } </main> <FooterComponent /> </> ); } export default LayoutComponent;
诉求
明确重渲染问题的根因,获取无需全量包裹React.memo的可行解决方案。
问题根因
异常重渲染由两个核心问题共同触发:
- next-auth
4.6.1版本的SessionProvider存在已知实现缺陷:路由跳转触发内部session同步逻辑时,即使session实际权限、用户信息等内容完全没有变化,内部生成的Context value也会返回新的对象引用,直接触发Context更新。按照React的Context渲染规则,只要Provider的value引用发生变更,整个Provider子树都会进入重渲染流程。 - 全局
LayoutComponent未做任何渲染缓存,会随父组件SessionProvider的重渲染同步执行重渲染,连带其内部所有未做memo的子节点(FooterComponent、路由页容器等)一起重渲染,只有提前包裹了React.memo的HeaderComponent因为传入props未变化跳过了重渲染,和profiler的检测结果完全吻合。
另外直接透传pageProps.session给SessionProvider也会放大问题:每次路由跳转Next.js生成的pageProps是全新对象,即使session内容完全一致,pageProps.session的引用也可能改变,额外触发Provider的无意义更新。
解决方案
以下方案组合使用即可解决问题,不需要给所有业务组件手动包裹React.memo:
- 升级next-auth版本
优先将next-auth升级到4.18.0以上的v4稳定版本,官方在后续迭代中修复了session未变更时Context value引用不稳定的问题,路由跳转时如果session无实际变化不会触发Context更新。 - 稳定传入SessionProvider的参数
用useMemo缓存传入的session值,同时关闭非必要的自动重取逻辑,减少Context无意义更新的触发场景,修改后的_app.tsx代码如下:import { useMemo } from 'react'; function App({ Component, pageProps }: AppProps) { // 仅当session实际内容变化时才生成新的引用 const stableSession = useMemo(() => pageProps.session, [pageProps.session]); return ( <> <Head> <title>foo</title> <meta name="description" content="Generated by create next app" /> <link rel="icon" href="/favicon.ico" /> </Head> <SessionProvider session={stableSession} // 关闭窗口聚焦时的自动重取,无相关需求可开启 refetchOnWindowFocus={false} refetchWhenOffline={false} // 如果不存在路由跳转时session动态变更的场景(比如后台实时改权限),可开启下面配置进一步减少更新 // refetchOnRouteChange={false} > <LayoutComponent> <Component {...pageProps} /> </LayoutComponent> </SessionProvider> </> ); } - 给全局Layout组件加单次memo缓存
仅需要给LayoutComponent包裹一层React.memo即可,不需要给其他子组件单独加缓存。由于路由跳转时传入Layout的children(即路由页组件)引用会变化,memo不会阻碍路由页面的正常渲染,但可以保证Layout内部的FooterComponent等固定组件不会随Context更新无意义重渲染,修改后的LayoutComponent代码如下:import { memo, PropsWithChildren } from 'react'; const LayoutComponent = memo(({ children }: PropsWithChildren) => { return ( <> <HeaderComponent homePath='/' signinPath='/auth/signin' aboutPath='/about' /> <main> { children } </main> <FooterComponent /> </> ); }); export default LayoutComponent;
内容的提问来源于stack exchange,提问作者dsal3389
相关产品推荐
相关产品推荐

