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

Next.js SSR/CSR电商App部分设备加载空白无报错求助

Next.js 15(App Router)电商应用在部分设备/地区出现空白页面(DOM存在但UI不渲染)

问题概述

开发了一款基于Next.js 15(App Router)的电商Web应用,核心特性包括:SSR页面渲染、SSR/CSR混合组件、动态路由、全栈MUI、无HTTP缓存、Redux-Toolkit+redux-persist持久化购物车状态,部署于Cloudflare Tunnel、Vercel及Hostinger VPS。

在部分设备上出现异常:页面加载时<title>可见、API调用成功,DevTools中能看到完整DOM结构,但页面始终空白;控制台无报错(仅某设备短暂出现this page has no node提示)。

项目依赖

"dependencies": {
  "@emotion/cache": "^11.14.0",
  "@emotion/react": "^11.14.0",
  "@emotion/server": "^11.11.0",
  "@emotion/styled": "^11.14.0",
  "@imagekit/next": "^2.1.0",
  "@mui/icons-material": "^7.0.2",
  "@mui/material": "^7.0.2",
  "@mui/material-nextjs": "^7.0.2",
  "@reduxjs/toolkit": "^2.7.0",
  "bson": "^6.10.3",
  "isomorphic-dompurify": "^2.24.0",
  "mongodb": "^6.16.0",
  "next": "^15.3.1",
  "next-seo": "^6.6.0",
  "nprogress": "^0.2.0",
  "react": "^19.1.0",
  "react-dom": "^19.1.0",
  "react-hook-form": "^7.56.3",
  "react-redux": "^9.2.0",
  "redux-persist": "^6.0.0",
  "swr": "^2.3.3",
  "zod": "^3.24.4"
},
"devDependencies": {
  "@eslint/eslintrc": "^3",
  "eslint": "^9",
  "eslint-config-next": "15.3.1"
}

核心代码片段

1. app/layout.tsx

// app/layout.tsx
import './globals.css';
import { Rubik } from 'next/font/google';
import Providers from './provider';

const rubik = Rubik({
  subsets: ['latin'],
  weight: ['400', '500', '700'],
  variable: '--font-rubik',
});

export const metadata = { title: 'title' };

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={rubik.variable} style={{ scrollBehavior: 'smooth' }}>
      <body suppressHydrationWarning>
        <Providers>
          <main>{children}</main>
        </Providers>
      </body>
    </html>
  );
}

2. Providers.jsx

'use client';
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
import { ThemeProvider } from '@mui/material/styles';
import { Provider } from 'react-redux';
import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
import DeviceListener from './components/utils/DeviceListener.jsx';
import DataFetcher from './components/utils/DataFetcher.jsx';
import theme from './theme.js';
import { store } from '@/app/lib/redux/store.js';
import PersistWrapper from '@/app/components/PersistWrapper.jsx';

// Emotion cache for MUI
const cache = createCache({ key: 'css', prepend: true });

export default function Providers({ children }) {
  return (
    <Provider store={store}>
      <PersistWrapper>
        <CacheProvider value={cache}>
          <AppRouterCacheProvider options={{ key: 'css' }}>
            <DeviceListener />
            <DataFetcher />
            <ThemeProvider theme={theme}>
              {children}
            </ThemeProvider>
          </AppRouterCacheProvider>
        </CacheProvider>
      </PersistWrapper>
    </Provider>
  );
}

3. DataFetcher.jsx

'use client';
import { useEffect, useImperativeHandle, forwardRef, useCallback, startTransition } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { setCategories, setProducts } from '@/app/lib/redux/actions/data.js';

async function fetchJson(url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Fetch error: ${res.status}`);
  return res.json();
}

const DataFetcher = forwardRef((props, ref) => {
  const dispatch = useDispatch();
  const { categories, products } = useSelector((state) => state.data);

  const loadData = useCallback(async () => {
    try {
      const [cats, prods] = await Promise.all([
        fetchJson('…categories-endpoint…'),
        fetchJson('…products-endpoint…'),
      ]);
      dispatch(setCategories(cats));
      dispatch(setProducts(prods));
    } catch (err) {
      console.error('DataFetcher loadData error', err);
    }
  }, [dispatch]);

  useEffect(() => {
    startTransition(loadData);
  }, [loadData]);

  useImperativeHandle(ref, () => ({ refetch: loadData }), [loadData]);

  return null;
});

export default DataFetcher;

4. DeviceListener.jsx

'use client';
import { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { setSmDev, setMdDev, setLgDev, resetDeviceState } from '@/app/lib/redux/actions/device.js';

const BREAKPOINTS = { sm: 600, md: 900, lg: 1200 };

function getDeviceCategory(width) {
  if (width > BREAKPOINTS.md) return 'lg';
  if (width > BREAKPOINTS.sm) return 'md';
  return 'sm';
}

export default function DeviceListener() {
  const dispatch = useDispatch();
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    const updateDevice = () => {
      setIsClient(true);
      if (!isClient) return;

      const cat = getDeviceCategory(window.innerWidth);

      dispatch(resetDeviceState());
      if (cat === 'lg') dispatch(setLgDev(true));
      else if (cat === 'md') dispatch(setMdDev(true));
      else dispatch(setSmDev(true));
    };

    updateDevice();
    window.addEventListener('resize', updateDevice);
    return () => {
      window.removeEventListener('resize', updateDevice);
      dispatch(resetDeviceState());
    };
  }, [dispatch, isClient]);

  return null;
}

5. PersistWrapper.js

'use client';
import { useEffect, useState } from 'react';
import { PersistGate } from 'redux-persist/integration/react';
import { persistor } from '@/app/lib/redux/store';

export default function PersistWrapper({ children }) {
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  if (!isClient) return null;
  return <PersistGate loading={null} persistor={persistor}>{children}</PersistGate>;
}

补充细节

  • 正常渲染设备上UI功能完好
  • 加拿大地区(服务器所在地)部分终端无法渲染React树,VPN无效
  • 控制台无报错,元素检查可见DOM与CSS,但UI空白
  • 开发环境可正常访问测试

怀疑点

  • 存在区域相关性:英美地区可正常加载,加拿大地区部分设备失败;开发与生产环境均存在设备特定的空白问题,截图显示CSS块存在、检查器有完整DOM内容

可能原因与解决方案

1. Emotion缓存重复配置导致Hydration冲突

Providers.jsx中同时使用了CacheProvider和AppRouterCacheProvider,Next.js 15的MUI集成仅需AppRouterCacheProvider,重复缓存会导致样式不生效或DOM渲染异常。

  • 修复:移除手动创建的CacheProvider,简化为:
    export default function Providers({ children }) {
      return (
        <Provider store={store}>
          <PersistWrapper>
            <AppRouterCacheProvider options={{ key: 'css' }}>
              <DeviceListener />
              <DataFetcher />
              <ThemeProvider theme={theme}>
                {children}
              </ThemeProvider>
            </AppRouterCacheProvider>
          </PersistWrapper>
        </Provider>
      );
    }
    

2. Redux-Persist初始化导致DOM断裂

PersistWrapper初始返回null,会清空SSR渲染的内容,后续PersistGate挂载后可能未触发重新渲染。

  • 修复:将初始返回值改为空占位元素,避免DOM完全断裂:
    if (!isClient) return <div></div>;
    

3. DeviceListener中的状态竞态

updateDevice函数中先设置isClient=true,再判断!isClient,此时状态未更新,导致设备状态未被正确dispatch,依赖设备状态的组件无法渲染。

  • 修复:移除isClient状态,直接在useEffect中执行设备检测(useEffect仅在客户端运行):
    export default function DeviceListener() {
      const dispatch = useDispatch();
    
      useEffect(() => {
        const updateDevice = () => {
          const cat = getDeviceCategory(window.innerWidth);
          dispatch(resetDeviceState());
          if (cat === 'lg') dispatch(setLgDev(true));
          else if (cat === 'md') dispatch(setMdDev(true));
          else dispatch(setSmDev(true));
        };
        updateDevice();
        window.addEventListener('resize', updateDevice);
        return () => {
          window.removeEventListener('resize', updateDevice);
          dispatch(resetDeviceState());
        };
      }, [dispatch]);
    
      return null;
    }
    

4. startTransition延迟数据加载

DataFetcher中使用startTransition包裹数据加载,会将数据标记为非紧急更新,依赖Redux状态的组件可能在数据加载完成前不渲染。

  • 修复:移除startTransition,直接调用loadData:
    useEffect(() => {
      loadData();
    }, [loadData]);
    

5. 区域CDN资源加载异常

加拿大地区可能存在Next.js字体或MUI资源的CDN节点问题:

  • 优化:为字体添加display: 'swap',避免字体加载阻塞渲染:
    const rubik = Rubik({
      subsets: ['latin'],
      weight: ['400', '500', '700'],
      variable: '--font-rubik',
      display: 'swap'
    });
    

6. 样式隐藏问题

检查全局样式或MUI主题是否存在display: none、opacity: 0等属性导致元素不可见:

  • 验证:在DevTools的Elements面板查看元素的计算样式,确认visibility、opacity、display等属性是否正常。

内容的提问来源于stack exchange,提问作者HUZAIFA IRFAN

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 01:59:52