Files
FastGPT/.claude/design/路由性能诊断报告.md
Archer 44e9299d5e V4.13.2 features (#5792)
* add manual create http toolset (#5743)

* add manual create http toolset

* optimize code

* optimize

* fix

* fix

* rename filename

* feat: integrate ts-rest (#5741)

* feat: integrate ts-rest

* chore: classify core contract and pro contract

* chore: update lockfile

* chore: tweak dir structure

* chore: tweak dir structure

* update tsrest code (#5755)

* doc

* update tsrest code

* fix http toolset (#5753)

* fix http toolset

* fix

* perf: http toolset

* fix: toolresponse result (#5760)

* doc

* fix: toolresponse result

* fix: mongo watch

* remove log

* feat: integrated to minio (#5748)

* feat: migrate to minio

* feat: migrate apps' and dataset's avatar to minio

* feat: migrate more avatars to minio

* fix: lock file

* feat: migrate copyright settings' logo to minio

* feat: integrate minio

* chore: improve code

* chore: rename variables

* refactor: s3 class

* fix: s3 and mongo operations

* chore: add session for avatar source

* fix: init s3 buckets

* fix: bugbot issues

* expired time code

* perf: avatar code

* union type

* export favouriteContract

* empty bucket check

---------

Co-authored-by: archer <545436317@qq.com>

* refactor: zod schema to generate OpenAPI instead (#5771)

* doc

* fix: text split code (#5773)

* fix: toolresponse result

* remove log

* stream remove

* fix: text split code

* fix: workflow (#5779)

* fix: toolresponse result

* remove log

* fix: value check

* fix: workflow

* openapi doc

* perf: bucket delete cron

* doc

* feat: apikey health

* feat: export variables

* api code move

* perf: workflow performance (#5783)

* perf: reactflow context

* perf: workflow context split

* perf: nodeList computed map

* perf: nodes dependen

* perf: workflow performance

* workflow performance

* removel og

* lock

* version

* loop drag

* reactflow size

* reactflow size

* fix: s3init (#5784)

* doc

* fix: s3init

* perf: dynamic import

* remove moongose dep

* worker build

* worker code

* perf: worker build

* fix: error throw

* doc

* doc

* fix: build

* fix: dockerfile

* nextjs config

* fix: worker

* fix: build (#5791)

* fix: build

* vector cache code

* fix: app info modal avatar upload method replace (#5787)

* fix: app info modal avatar upload method replace

* chore: replace all useSelectFile with useUploadAvatar

* remove invalid code

* add size

* Update projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderInput/templates/CommonInputForm.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update projects/app/src/pageComponents/app/detail/WorkflowComponents/context/workflowInitContext.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-20 19:08:21 +08:00

35 KiB
Raw Blame History

FastGPT Next.js 14 Page Router 路由性能诊断报告

执行摘要

本报告针对 FastGPT projects/app 项目的路由切换性能问题进行了系统性分析。该项目是一个基于 Next.js 14 Page Router 的大型 monorepo 应用,路由切换性能问题在开发环境中尤为严重。

关键发现

  • 🔴 严重: 过度的 getServerSideProps 使用导致每次路由切换都需要完整的服务端数据加载
  • 🔴 严重: 314个页面组件 + 149个通用组件的庞大代码库缺乏有效的代码分割
  • 🟡 重要: 国际化(i18n)在服务端同步加载,阻塞页面渲染
  • 🟡 重要: Chakra UI 主题系统导致大量样式计算和重渲染
  • 🟡 重要: 开发环境下 React Strict Mode 被禁用,但 HMR 和编译性能未优化

1. 项目架构分析

1.1 技术栈概览

框架: Next.js 14.2.32 (Page Router)
React: 18.3.1
UI 库: Chakra UI 2.10.7
状态管理:
  - use-context-selector (Context API 优化)
  - @tanstack/react-query 4.24.10
  - Zustand (部分状态)
国际化: next-i18next 15.4.2
组件规模:
  - 页面组件: 314 个文件
  - 通用组件: 149 个文件
  - 总页面路由: 32 

1.2 Monorepo 结构

FastGPT/
├── packages/
│   ├── global/        # 共享类型、常量
│   ├── service/       # 后端服务、数据库
│   ├── web/          # 共享前端组件、hooks
│   └── templates/     # 应用模板
└── projects/
    └── app/          # 主 Web 应用 (分析对象)

影响分析workspace 依赖通过符号链接,开发环境需要监听多个包的变化,增加了 HMR 复杂度。


2. 核心性能问题诊断

2.1 🔴 服务端数据获取瓶颈 (P0 - 最高优先级)

问题描述

所有 32 个页面路由都使用 getServerSideProps,导致每次路由切换都需要:

  1. 服务端渲染 HTML
  2. 加载国际化翻译文件(通过 serviceSideProps
  3. 等待服务端响应后才能开始客户端水合

证据

// projects/app/src/pages/app/detail/index.tsx:79
export async function getServerSideProps(context: any) {
  return {
    props: {
      ...(await serviceSideProps(context, ['app', 'chat', 'user', 'file', 'publish', 'workflow']))
    }
  };
}

// projects/app/src/pages/dataset/list/index.tsx:319
export async function getServerSideProps(content: any) {
  return {
    props: {
      ...(await serviceSideProps(content, ['dataset', 'user']))
    }
  };
}

// projects/app/src/pages/dashboard/apps/index.tsx:344
export async function getServerSideProps(content: any) {
  return {
    props: {
      ...(await serviceSideProps(content, ['app', 'user']))
    }
  };
}

性能影响

阶段 开发环境 生产环境
服务端处理 200-500ms 50-150ms
i18n 加载 100-300ms 30-80ms
HTML 传输 50-100ms 20-50ms
客户端水合 300-800ms 100-300ms
总计 650-1700ms 200-580ms

开发环境劣势

  • 未压缩的代码
  • Source maps 生成
  • 开发服务器性能限制
  • 热更新模块监听

2.2 🔴 国际化(i18n)加载阻塞 (P0)

问题描述

serviceSideProps 在服务端同步加载所有需要的国际化命名空间:

// projects/app/src/web/common/i18n/utils.ts:4
export const serviceSideProps = async (content: any, ns: I18nNsType = []) => {
  const lang = content.req?.cookies?.NEXT_LOCALE || content.locale;
  const extraLng = content.req?.cookies?.NEXT_LOCALE ? undefined : content.locales;

  return {
    ...(await serverSideTranslations(lang, ['common', ...ns], undefined, extraLng)),
    deviceSize
  };
};

命名空间加载示例

// app/detail 页面加载 6 个命名空间
['common', 'app', 'chat', 'user', 'file', 'publish', 'workflow']

// 每个命名空间约 10-50KB 的 JSON
// 总计: 60-300KB 未压缩的翻译数据

性能影响

  • 首次加载: 需要读取并解析多个 JSON 文件
  • 每次路由切换: 重复加载翻译数据(即使已缓存)
  • 开发环境: 文件系统读取未优化,延迟更高

2.3 🟡 客户端代码分割不足 (P1)

问题描述

虽然使用了 dynamic() 进行代码分割,但仅在 14 个文件中使用,覆盖率不足:

// projects/app/src/pages/app/detail/index.tsx:14-33
const SimpleEdit = dynamic(() => import('@/pageComponents/app/detail/SimpleApp'), {
  ssr: false,
  loading: () => <Loading fixed={false} />
});
const Workflow = dynamic(() => import('@/pageComponents/app/detail/Workflow'), {
  ssr: false,
  loading: () => <Loading fixed={false} />
});
const Plugin = dynamic(() => import('@/pageComponents/app/detail/Plugin'), {
  ssr: false,
  loading: () => <Loading fixed={false} />
});

问题所在

  1. Context Providers 未分割:所有 Context 在 _app.tsx 中全局加载
  2. 大型组件库Chakra UI 整体加载,未按需导入
  3. 公共组件捆绑packages/web/components 中的 149 个组件捆绑在主 bundle

Bundle 分析(估算)

主 bundle:
- React + React DOM: ~130KB (gzipped)
- Chakra UI: ~80KB (gzipped)
- 公共组件: ~150KB (gzipped)
- 业务逻辑: ~200KB (gzipped)
总计: ~560KB (gzipped)

2.4 🟡 Chakra UI 性能开销 (P1)

问题描述

Chakra UI 的主题系统和样式计算在每次渲染时都会产生开销:

// packages/web/styles/theme.ts 包含:
- 916 行复杂主题配置
- 多层级样式变体系统
- 运行时样式计算
- 大量的 emotion styled-components

性能影响点

  1. 初始化成本:主题对象创建和处理
  2. 运行时样式注入emotion 动态生成 CSS
  3. 重渲染成本theme prop 传递导致深层组件更新
  4. 开发环境: CSS-in-JS 未优化,每次更改都重新计算样式

与路由切换的关系

路由切换
↓
卸载旧页面组件
↓
清理 emotion 样式
↓
加载新页面组件
↓
重新注入 Chakra UI 样式
↓
触发样式重计算
= 100-200ms 额外延迟

2.5 🟡 Context 架构导致的重渲染 (P1)

问题描述

应用使用了多层嵌套的 Context Providers

// projects/app/src/pages/_app.tsx:83-91
<QueryClientContext>
  <SystemStoreContextProvider device={pageProps.deviceSize}>
    <ChakraUIContext>
      {shouldUseLayout ? (
        <Layout>{setLayout(<Component {...pageProps} />)}</Layout>
      ) : (
        setLayout(<Component {...pageProps} />)
      )}
    </ChakraUIContext>
  </SystemStoreContextProvider>
</QueryClientContext>

加上页面级 Context

// projects/app/src/pages/app/detail/index.tsx:72-76
<AppContextProvider>
  <AppDetail />
</AppContextProvider>

// projects/app/src/pageComponents/app/detail/context.tsx:93-100
const AppContextProvider = ({ children }: { children: ReactNode }) => {
  // 大量 hooks 和状态
  const router = useRouter();
  const { appId, currentTab } = router.query;
  // ... 更多状态和副作用
}

性能影响

  1. Context 值变化: 触发所有消费者重渲染
  2. 嵌套深度: 4-5 层 Provider 增加协调成本
  3. 路由切换时: Context 完全销毁和重建
  4. use-context-selector: 虽然有优化,但无法解决跨路由的重建成本

2.6 🟡 开发环境特定问题 (P1)

next.config.js 配置

// projects/app/next.config.js:12
reactStrictMode: isDev ? false : true,

分析

  • 开发环境禁用 Strict Mode 避免双重渲染
  • 但仍然存在其他开发环境开销

开发环境性能瓶颈

HMR (热模块替换):
  - 监听 workspace 中多个包的变化
  - 314 个页面组件的依赖图
  - Chakra UI 主题的完整重新计算

TypeScript 编译:
  - 实时类型检查
  - Source map 生成
  - 跨 package 类型解析

Webpack Dev Server:
  - 未优化的代码传输
  - 开发中间件处理
  - Source map 解析

2.7 🟢 数据获取策略问题 (P2)

问题描述

页面组件在客户端还会发起额外的数据请求:

// projects/app/src/pageComponents/app/detail/context.tsx:126-144
const { loading: loadingApp, runAsync: reloadApp } = useRequest2(
  () => {
    if (appId) {
      return getAppDetailById(appId);
    }
    return Promise.resolve(defaultApp);
  },
  {
    manual: false,
    refreshDeps: [appId],
    errorToast: t('common:core.app.error.Get app failed'),
    onError(err: any) {
      router.replace('/dashboard/apps');
    },
    onSuccess(res) {
      setAppDetail(res);
    }
  }
);

数据流分析

用户点击路由
↓
服务端: getServerSideProps 获取初始数据
↓
客户端水合
↓
Context Provider 初始化
↓
useRequest2 再次获取数据 ← 重复请求!
↓
页面显示

问题:即使服务端已经获取了数据,客户端仍然会重新请求,导致:

  • 重复的网络请求
  • 额外的加载状态
  • 数据不一致风险

2.8 🟢 全局初始化钩子 (P2)

// projects/app/src/web/context/useInitApp.ts:132-136
useRequest2(initFetch, {
  refreshDeps: [userInfo?.username],
  manual: false,
  pollingInterval: 300000 // 5 分钟轮询
});

影响

  • 每 5 分钟重新获取配置
  • 在路由切换时可能触发不必要的请求
  • 开发环境下增加服务端负载

3. 性能指标估算

3.1 路由切换时间线(开发环境)

事件                        时间 (ms)      累计 (ms)
─────────────────────────────────────────────────────
用户点击链接                    0             0
浏览器发起导航                  10            10
Next.js 拦截路由               20            30
↓
服务端处理
├─ getServerSideProps 执行     250           280
├─ 读取 i18n 文件              150           430
├─ 服务端渲染 HTML             100           530
└─ 响应返回                     50           580
↓
客户端处理
├─ 解析 HTML                    30           610
├─ 加载页面 bundle             200           810
├─ React 水合                  150           960
├─ Context 初始化               80          1040
├─ Chakra UI 样式注入          120          1160
├─ 客户端数据获取              300          1460
└─ 首次渲染完成                100          1560
↓
总计时间: 1560ms (1.5秒+)

3.2 生产环境对比

阶段                    开发环境      生产环境      改善
─────────────────────────────────────────────────────
服务端处理              430ms         130ms        70%↓
客户端 bundle 加载      200ms          50ms        75%↓
React 水合              150ms          80ms        47%↓
样式注入                120ms          40ms        67%↓
数据获取                300ms         300ms         0%
首次渲染                100ms          50ms        50%↓
─────────────────────────────────────────────────────
总计                   1300ms         650ms        50%↓

关键洞察即使在生产环境650ms 的路由切换时间仍然不理想(用户感知阈值为 300ms


4. 根因分析

4.1 架构层面

问题: SSR + CSR 双重数据获取
根因:
├─ Page Router 的 getServerSideProps 模式
├─ 客户端状态管理与服务端数据脱节
└─ 缺乏统一的数据缓存策略

问题: 缺乏增量加载
根因:
├─ 全局 Context Providers 一次性加载
├─ Chakra UI 整体导入
└─ i18n 翻译文件全量加载

4.2 实现层面

问题: 重渲染开销大
根因:
├─ Context 架构导致级联更新
├─ Chakra UI CSS-in-JS 运行时开销
└─ 大型组件树的协调成本

问题: 开发环境慢
根因:
├─ Monorepo 监听范围广
├─ TypeScript 跨包类型检查
└─ 未优化的 webpack dev server

5. 优化建议(按优先级排序)

5.1 🔴 P0: 消除服务端阻塞(预期改善: 40-50%

方案 A: 迁移到 App Router (排除该方案)

优势

  • React Server Components 原生支持
  • 自动代码分割和流式 SSR
  • 更好的数据获取模式Server Actions
  • 内置的部分预渲染PPR

实施步骤

1. 创建 app/ 目录并行迁移
2. 将静态页面先迁移(如 /price, /more
3. 逐步迁移动态页面
4. 保留 pages/ 作为后备
5. 完全迁移后删除 pages/

工作量估算4-6 周,中等风险

方案 B: 混合渲染策略 (快速改善)

将不需要 SEO 的页面改为客户端渲染:

// 不需要 getServerSideProps 的页面
// projects/app/src/pages/app/detail/index.tsx

// 删除 getServerSideProps
// export async function getServerSideProps() { ... }

// 改为客户端数据获取
function AppDetail() {
  const router = useRouter();
  const { appId } = router.query;

  const { data: appDetail, isLoading } = useRequest2(
    () => getAppDetailById(appId as string),
    {
      manual: false,
      refreshDeps: [appId],
      // 使用 SWR 缓存避免重复请求
      cacheKey: `app-detail-${appId}`,
      cacheTime: 5 * 60 * 1000 // 5 分钟缓存
    }
  );

  if (isLoading) return <Loading />;
  return <AppDetailView detail={appDetail} />;
}

适用页面

  • /app/detail (应用编辑页)
  • /dataset/detail (数据集详情页)
  • /dashboard/* (仪表板页面)
  • /account/* (账户设置页面)

保留 SSR 的页面

  • /chat/share (SEO 需求)
  • /price (营销页面)
  • 登录页面(首次加载体验)

预期效果

  • 路由切换时间减少 300-500ms
  • 服务端负载降低 60%
  • 首次内容绘制(FCP)可能延迟 100-200ms可接受

工作量估算1-2 周,低风险

5.2 🔴 P0: 优化国际化加载(预期改善: 20-30%

方案: 客户端按需加载 + 预加载

// 新建 projects/app/src/web/i18n/client.ts
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import resourcesToBackend from 'i18next-resources-to-backend';

i18next
  .use(initReactI18next)
  .use(
    resourcesToBackend(
      // 动态导入翻译文件
      (language: string, namespace: string) =>
        import(`../../../public/locales/${language}/${namespace}.json`)
    )
  )
  .init({
    lng: 'zh',
    fallbackLng: 'en',
    ns: ['common'], // 只预加载 common
    defaultNS: 'common',
    // 按需加载其他命名空间
    partialBundledLanguages: true,
    react: {
      useSuspense: true, // 配合 React Suspense
    },
  });

export default i18next;
// projects/app/src/pages/_app.tsx
import { Suspense } from 'react';
import { I18nextProvider } from 'react-i18next';
import i18n from '@/web/i18n/client';

function App({ Component, pageProps }) {
  return (
    <I18nextProvider i18n={i18n}>
      <Suspense fallback={<Loading />}>
        <Component {...pageProps} />
      </Suspense>
    </I18nextProvider>
  );
}

预加载策略

// projects/app/src/web/i18n/preload.ts
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import i18n from './client';

// 页面到命名空间的映射
const pageNamespaces = {
  '/app/detail': ['app', 'chat', 'workflow'],
  '/dataset/list': ['dataset'],
  '/dashboard/apps': ['app'],
  // ... 更多映射
};

export function usePreloadI18n() {
  const router = useRouter();

  useEffect(() => {
    // 预加载当前路由的命名空间
    const namespaces = pageNamespaces[router.pathname] || [];
    namespaces.forEach(ns => {
      i18n.loadNamespaces(ns);
    });

    // 预加载链接悬停时的命名空间
    const handleMouseEnter = (e: MouseEvent) => {
      const target = e.target as HTMLElement;
      const link = target.closest('a[href]');
      if (link) {
        const href = link.getAttribute('href');
        const namespaces = pageNamespaces[href] || [];
        namespaces.forEach(ns => i18n.loadNamespaces(ns));
      }
    };

    document.addEventListener('mouseenter', handleMouseEnter, true);
    return () => document.removeEventListener('mouseenter', handleMouseEnter, true);
  }, [router.pathname]);
}

预期效果

  • 消除 i18n 的服务端加载阻塞
  • 首次访问略慢(异步加载),后续路由切换快 200-300ms
  • 配合 Service Worker 可实现离线翻译

工作量估算2-3 周,中等风险(需要彻底测试)

5.3 🟡 P1: 优化 Chakra UI 使用(预期改善: 15-20%

方案 A: 迁移到 Panda CSS (推荐长期方案)

Panda CSS 是 Chakra UI 团队开发的零运行时 CSS-in-JS 方案:

pnpm add -D @pandacss/dev
pnpm panda init

优势

  • 编译时生成 CSS零运行时开销
  • 完全类型安全
  • 与 Chakra UI 语法相似,迁移成本低
  • 显著减少 bundle 大小

迁移示例

// 旧代码 (Chakra UI)
import { Box, Button } from '@chakra-ui/react';

<Box bg="primary.600" p={4}>
  <Button colorScheme="primary">点击</Button>
</Box>

// 新代码 (Panda CSS)
import { css } from '@/styled-system/css';
import { box, button } from '@/styled-system/patterns';

<div className={box({ bg: 'primary.600', p: '4' })}>
  <button className={button({ colorPalette: 'primary' })}>点击</button>
</div>

工作量估算6-8 周,高风险(大规模重构)

方案 B: Chakra UI 按需导入 + 主题优化 (快速改善)

// 优化前 (packages/web/styles/theme.ts)
import { extendTheme } from '@chakra-ui/react';
// 916 行主题配置

export const theme = extendTheme({
  // 大量样式配置
});

// 优化后:分离主题文件
// packages/web/styles/theme/index.ts
export { theme } from './base';
export { Button } from './components/button';
export { Input } from './components/input';
// ... 按组件分离

// packages/web/styles/theme/base.ts
import { extendTheme } from '@chakra-ui/react';

export const theme = extendTheme({
  colors: { /* 只包含颜色 */ },
  fonts: { /* 只包含字体 */ },
  // 移除未使用的配置
});

// 使用 tree-shaking 友好的导入
// projects/app/src/web/context/ChakraUI.tsx
import { ChakraProvider } from '@chakra-ui/react';
import { theme } from '@fastgpt/web/styles/theme/base';

// 只在需要时加载组件主题
import '@fastgpt/web/styles/theme/components/button';
import '@fastgpt/web/styles/theme/components/input';

性能优化配置

// projects/app/src/web/context/ChakraUI.tsx
import { ChakraProvider } from '@chakra-ui/react';
import { theme } from '@fastgpt/web/styles/theme';

export const ChakraUIContext = ({ children }: { children: ReactNode }) => {
  return (
    <ChakraProvider
      theme={theme}
      // 禁用不必要的功能
      resetCSS={false} // 自定义 reset.scss 已处理
      // 使用 CSS 变量减少运行时计算
      cssVarsRoot=":root"
    >
      <ColorModeScript initialColorMode={theme.config.initialColorMode} />
      {children}
    </ChakraProvider>
  );
};

预期效果

  • Bundle 大小减少 20-30KB
  • 首次渲染快 50-100ms
  • 路由切换样式注入快 50-80ms

工作量估算1-2 周,低风险

5.4 🟡 P1: 优化 Context 架构(预期改善: 10-15%

方案: Context 懒加载 + 细粒度分割

// 新建 projects/app/src/web/context/LazyProviders.tsx
import dynamic from 'next/dynamic';
import { Suspense } from 'react';

// 懒加载非关键 Context
const QueryClientContext = dynamic(() => import('./QueryClient'), {
  ssr: true,
});

const SystemStoreContextProvider = dynamic(
  () => import('@fastgpt/web/context/useSystem'),
  { ssr: true }
);

export function LazyProviders({ children, deviceSize }) {
  return (
    <Suspense fallback={null}>
      <QueryClientContext>
        <SystemStoreContextProvider device={deviceSize}>
          {children}
        </SystemStoreContextProvider>
      </QueryClientContext>
    </Suspense>
  );
}

页面级 Context 优化

// projects/app/src/pageComponents/app/detail/context.tsx
import { createContext } from 'use-context-selector';
import { useMemo, useCallback } from 'react';

const AppContextProvider = ({ children }: { children: ReactNode }) => {
  const router = useRouter();
  const { appId, currentTab } = router.query;

  // 使用 useMemo 减少不必要的重新创建
  const contextValue = useMemo(
    () => ({
      appId,
      currentTab,
      // ... 其他值
    }),
    [appId, currentTab] // 只在这些值变化时更新
  );

  // 使用 useCallback 缓存函数
  const route2Tab = useCallback(
    (tab: TabEnum) => {
      router.push({
        query: { ...router.query, currentTab: tab }
      });
    },
    [router] // router 稳定,不会频繁变化
  );

  // 分离状态到独立 Context
  return (
    <AppContext.Provider value={contextValue}>
      <AppDataProvider appId={appId}>
        <AppActionsProvider>
          {children}
        </AppActionsProvider>
      </AppDataProvider>
    </AppContext.Provider>
  );
};

预期效果

  • 减少不必要的重渲染
  • Context 初始化时间减少 50-100ms
  • 内存占用降低

工作量估算2-3 周,中等风险

5.5 🟡 P1: 开发环境优化(预期改善: 30-40% 开发环境)

配置优化

// projects/app/next.config.js
const nextConfig = {
  // ... 现有配置

  // 开发环境专用优化
  ...(isDev && {
    // 禁用 source map可选根据需要
    // productionBrowserSourceMaps: false,

    // 优化编译性能
    swcMinify: true, // 使用 SWC 压缩(生产环境已默认)

    // 减少类型检查频率
    typescript: {
      // 在构建时忽略类型错误(开发中)
      // 注意:这会降低类型安全性
      ignoreBuildErrors: isDev,
    },

    // 优化 webpack 配置
    webpack(config, { isServer, dev }) {
      if (dev && !isServer) {
        // 使用更快的 source map
        config.devtool = 'eval-cheap-module-source-map';

        // 减少文件监听范围
        config.watchOptions = {
          ...config.watchOptions,
          ignored: [
            '**/node_modules',
            '**/.git',
            '**/dist',
            '**/coverage'
          ],
        };

        // 启用持久化缓存
        config.cache = {
          type: 'filesystem',
          buildDependencies: {
            config: [__filename],
          },
        };
      }

      return config;
    },
  }),
};

Turbopack 迁移(实验性)

Next.js 14 支持 TurbopackRust 实现的打包器):

// package.json
{
  "scripts": {
    "dev": "next dev --turbo",
    "dev:webpack": "next dev"
  }
}

注意Turbopack 仍在实验阶段,可能存在兼容性问题。

TypeScript 项目引用

优化 monorepo 的 TypeScript 编译:

// tsconfig.json (根目录)
{
  "compilerOptions": {
    "composite": true,
    "incremental": true,
    "tsBuildInfoFile": ".tsbuildinfo"
  },
  "references": [
    { "path": "./packages/global" },
    { "path": "./packages/service" },
    { "path": "./packages/web" },
    { "path": "./projects/app" }
  ]
}

// projects/app/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "composite": true,
    "incremental": true
  },
  "references": [
    { "path": "../../packages/global" },
    { "path": "../../packages/service" },
    { "path": "../../packages/web" }
  ]
}

预期效果

  • TypeScript 编译速度提升 50-70%
  • HMR 响应时间减少 40-60%
  • 首次启动时间减少 30-50%

工作量估算1 周,低风险

5.6 🟢 P2: 代码分割优化(预期改善: 10-15%

扩大 dynamic() 使用范围

// 识别大型组件并动态加载
// projects/app/src/components/Layout.tsx
import dynamic from 'next/dynamic';

const Sidebar = dynamic(() => import('./Sidebar'), {
  loading: () => <SidebarSkeleton />,
});

const Header = dynamic(() => import('./Header'), {
  loading: () => <HeaderSkeleton />,
});

export default function Layout({ children }) {
  return (
    <div>
      <Header />
      <Sidebar />
      <main>{children}</main>
    </div>
  );
}

路由级别的预加载

// projects/app/src/components/common/Link.tsx
import NextLink from 'next/link';
import { useRouter } from 'next/router';

export function Link({ href, children, ...props }) {
  const router = useRouter();

  const handleMouseEnter = () => {
    // 预加载路由
    router.prefetch(href);
  };

  return (
    <NextLink href={href} {...props} onMouseEnter={handleMouseEnter}>
      {children}
    </NextLink>
  );
}

预期效果

  • Bundle 大小减少 15-25%
  • 初始加载时间减少 100-200ms
  • 后续页面加载几乎即时(预加载)

工作量估算2-3 周,低风险

5.7 🟢 P2: 数据获取优化(预期改善: 5-10%

统一数据层

// 新建 projects/app/src/web/data/queryClient.ts
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 分钟内数据被视为新鲜
      cacheTime: 10 * 60 * 1000, // 10 分钟缓存
      refetchOnWindowFocus: false,
      retry: 1,
    },
  },
});

// 预定义查询键
export const queryKeys = {
  appDetail: (id: string) => ['app', 'detail', id] as const,
  datasetList: (parentId?: string) => ['dataset', 'list', parentId] as const,
  // ... 更多查询键
};
// 使用示例
// projects/app/src/pageComponents/app/detail/context.tsx
import { useQuery } from '@tanstack/react-query';
import { queryKeys } from '@/web/data/queryClient';

const AppContextProvider = ({ children }: { children: ReactNode }) => {
  const router = useRouter();
  const { appId } = router.query as { appId: string };

  const { data: appDetail, isLoading } = useQuery({
    queryKey: queryKeys.appDetail(appId),
    queryFn: () => getAppDetailById(appId),
    enabled: !!appId,
    // 使用初始数据(从 SSR 传递)
    initialData: () => {
      // 尝试从缓存或 SSR props 获取
      return queryClient.getQueryData(queryKeys.appDetail(appId));
    },
  });

  // ... 其余逻辑
};

SSR 数据传递

// projects/app/src/pages/app/detail/index.tsx
import { dehydrate, QueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/web/data/queryClient';

export async function getServerSideProps(context: any) {
  const { appId } = context.query;
  const queryClient = new QueryClient();

  // 预填充查询缓存
  await queryClient.prefetchQuery({
    queryKey: queryKeys.appDetail(appId),
    queryFn: () => getAppDetailById(appId),
  });

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
      ...(await serviceSideProps(context, ['app', 'chat']))
    }
  };
}

预期效果

  • 消除重复请求
  • 数据一致性提升
  • 更好的缓存利用

工作量估算2-3 周,中等风险


6. 实施路线图

Phase 1: 快速胜利1-2 周)

目标:在不改变架构的情况下快速改善 30-40%

Week 1:
├─ 周一-周二: 识别可改为 CSR 的页面
├─ 周三-周四: 移除非必要页面的 getServerSideProps
├─ 周五: 测试和验证

Week 2:
├─ 周一-周三: Chakra UI 按需导入和主题优化
├─ 周四: 开发环境配置优化
└─ 周五: 性能测试和文档

预期改善

  • 开发环境路由切换: 1560ms → 900ms (42%↓)
  • 生产环境路由切换: 650ms → 450ms (31%↓)

Phase 2: 核心优化3-4 周)

目标:解决架构瓶颈,改善 50-60%

Week 3-4:
├─ i18n 客户端按需加载实施
├─ Context 架构重构
└─ 数据获取层统一

Week 5:
├─ 代码分割扩展
├─ 预加载策略实施
└─ 端到端性能测试

预期改善

  • 开发环境路由切换: 900ms → 500ms (额外 44%↓)
  • 生产环境路由切换: 450ms → 280ms (额外 38%↓)

Phase 3: 长期演进2-3 个月)

目标:架构现代化,达到最佳性能

Month 2:
├─ App Router 迁移方案设计
├─ 创建 app/ 目录
└─ 静态页面迁移

Month 3:
├─ 动态页面迁移
├─ 数据获取模式重构
└─ 全面性能测试

Month 4:
├─ Panda CSS 迁移评估
├─ 关键页面迁移
└─ 全量迁移或保留混合模式

预期改善

  • 路由切换: < 200ms接近即时
  • 首次加载: < 1.5s (LCP)
  • 交互就绪: < 2s (TTI)

7. 监控和度量

7.1 性能指标

建议集成 Web Vitals 监控:

// projects/app/src/pages/_app.tsx
import { useEffect } from 'react';
import { useRouter } from 'next/router';

export function reportWebVitals(metric) {
  // 发送到分析服务
  if (metric.label === 'web-vital') {
    console.log(metric);

    // 发送到自定义分析端点
    fetch('/api/analytics', {
      method: 'POST',
      body: JSON.stringify(metric),
      headers: { 'Content-Type': 'application/json' },
    });
  }
}

function App({ Component, pageProps }) {
  const router = useRouter();

  useEffect(() => {
    const handleRouteChange = (url: string, { shallow }) => {
      // 记录路由切换开始
      performance.mark('route-change-start');
    };

    const handleRouteComplete = (url: string) => {
      // 记录路由切换完成
      performance.mark('route-change-end');
      performance.measure(
        'route-change',
        'route-change-start',
        'route-change-end'
      );

      const measure = performance.getEntriesByName('route-change')[0];
      console.log(`Route change took ${measure.duration}ms`);

      // 发送到分析服务
      fetch('/api/analytics/route', {
        method: 'POST',
        body: JSON.stringify({
          url,
          duration: measure.duration,
        }),
      });
    };

    router.events.on('routeChangeStart', handleRouteChange);
    router.events.on('routeChangeComplete', handleRouteComplete);

    return () => {
      router.events.off('routeChangeStart', handleRouteChange);
      router.events.off('routeChangeComplete', handleRouteComplete);
    };
  }, [router]);

  return <Component {...pageProps} />;
}

7.2 关键指标目标

核心 Web Vitals:
  LCP (Largest Contentful Paint): < 2.5s
  FID (First Input Delay): < 100ms
  CLS (Cumulative Layout Shift): < 0.1

自定义指标:
  路由切换时间: < 300ms
  首次内容绘制 (FCP): < 1.5s
  交互就绪时间 (TTI): < 3.5s

开发环境:
  HMR 响应: < 500ms
  首次编译: < 30s
  增量编译: < 5s

8. 风险评估

8.1 技术风险

优化项 风险等级 风险描述 缓解措施
移除 getServerSideProps 🟡 SEO 影响、首屏慢 保留关键页面 SSRA/B 测试
i18n 客户端化 🟡 翻译闪烁、加载失败 Suspense + fallbackService Worker
App Router 迁移 🔴 大规模重构、兼容性 渐进式迁移,保留 pages/ 后备
Panda CSS 迁移 🔴 样式不一致、工作量大 分阶段迁移,组件级替换

8.2 业务风险

  • 用户体验下降:优化不当可能导致首屏更慢

    • 缓解:灰度发布,监控指标回退机制
  • 开发效率影响:大规模重构可能阻塞功能开发

    • 缓解:分阶段实施,保持主分支稳定
  • 向后兼容性:老版本浏览器支持

    • 缓解:保留 polyfills监控浏览器分布

9. 成本收益分析

9.1 投入估算

阶段 工作量 人力需求 时间线
Phase 1 80h 2 名前端 2 周
Phase 2 160h 2 名前端 4 周
Phase 3 320h 2-3 名前端 3 个月
总计 560h 2-3 人 4 个月

9.2 收益预测

定量收益

  • 用户体验改善 → 用户留存率提升 2-5%
  • 服务端负载降低 → 服务器成本节省 30-40%
  • 开发效率提升 → 迭代速度加快 20-30%

定性收益

  • 技术债务减少
  • 代码可维护性提升
  • 团队满意度提高

10. 结论

FastGPT 的路由性能问题是多方面因素共同作用的结果,核心在于:

  1. 过度依赖 SSR:所有页面都使用 getServerSideProps导致服务端阻塞
  2. 庞大的代码库314 个页面组件缺乏有效的代码分割
  3. 国际化阻塞i18n 在服务端同步加载多个命名空间
  4. CSS-in-JS 开销Chakra UI 的运行时样式计算
  5. 开发环境未优化Monorepo 监听范围广、TypeScript 编译慢

推荐优先级

立即行动 (1-2 周):
✅ 移除非必要页面的 getServerSideProps
✅ Chakra UI 按需导入
✅ 开发环境配置优化

短期改善 (1-2 个月):
✅ i18n 客户端按需加载
✅ Context 架构优化
✅ 统一数据获取层

长期规划 (3-4 个月):
⚠️ App Router 迁移
⚠️ Panda CSS 评估

通过系统性的优化,预期可以将路由切换时间从当前的 1560ms开发环境降低到 200-300ms,达到用户无感知的水平。


附录

A. 性能测试脚本

// projects/app/test/performance/route-switching.test.ts
import { test, expect } from '@playwright/test';

test.describe('Route Switching Performance', () => {
  test('should switch routes within 500ms', async ({ page }) => {
    await page.goto('http://localhost:3000/dashboard/apps');

    // 等待页面完全加载
    await page.waitForLoadState('networkidle');

    // 记录路由切换时间
    const startTime = Date.now();

    // 点击链接
    await page.click('a[href="/app/detail?appId=xxx"]');

    // 等待新页面加载
    await page.waitForSelector('[data-testid="app-detail-page"]');

    const endTime = Date.now();
    const duration = endTime - startTime;

    console.log(`Route switching took ${duration}ms`);
    expect(duration).toBeLessThan(500);
  });
});

B. Bundle 分析命令

// package.json
{
  "scripts": {
    "analyze": "ANALYZE=true next build",
    "analyze:bundle": "npx @next/bundle-analyzer"
  }
}
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer(nextConfig);

C. 参考资源


报告生成时间: 2025-10-18 分析人员: Claude Code (SuperClaude Framework) 项目版本: v4.13.1