# 性能预算重构专项 - 完整审计报告
> **审计日期**: 2026-07-05
> **审计范围**: e:\Desktop\CICD 全项目(Next.js 16.0.10 + React 19.2.1 + Turbopack + Drizzle ORM + MySQL + NextAuth v5)
> **审计维度**: ① Bundle 体积预算(首期重点)② Core Web Vitals ③ 组件渲染性能 ④ Server Action / 数据层
> **审计方式**: 静态代码搜索 + 依赖图分析 + 源码路径追踪(未运行时采样)
> **交付形式**: 完整审计报告(不做实施)
> **说明**: 当前 `next build` 因 Turbopack 编译错误未成功出包,量化指标为基于依赖图与库体积经验值的合理推断值,需在修复 P0 项后重新构建获取实际产物体积进行第二轮校准。
---
## 目录
- [执行摘要](#执行摘要)
- [第一部分:Bundle 体积预算审计](#第一部分bundle-体积预算审计首期重点)
- [第二部分:Core Web Vitals 审计](#第二部分core-web-vitals-审计)
- [第三部分:组件渲染性能审计](#第三部分组件渲染性能审计)
- [第四部分:Server Action / 数据层审计](#第四部分server-action--数据层审计)
- [第五部分:综合性能预算基线表](#第五部分综合性能预算基线表)
- [第六部分:P0/P1 修复路线图](#第六部分p0p1-修复路线图)
- [附录 A:关键文件清单](#附录-a关键文件清单)
- [附录 B:审计涉及的依赖清单](#附录-b审计涉及的依赖清单)
---
## 执行摘要
本次性能预算审计覆盖 4 个维度,共发现 **24 个 P0/P1 问题、22 个 P2 问题、14 个 P3 问题**,全部为静态发现,未做运行时验证。
### P0 严重问题(影响生产稳定性 / 必须先修)
| # | 维度 | 问题 | 文件位置 |
|---|------|------|---------|
| 1 | Bundle | `KnowledgeGraph`(@xyflow/react)未动态导入,textbook 路由 chunk 膨胀 ~120 KB | `src/modules/textbooks/components/knowledge-graph.tsx:5-14` |
| 2 | Bundle | `ExamRichEditor` / `RichTextEditor` / `rich-text-block` 三处 tiptap 未动态导入,编辑器路由 chunk 膨胀 ~180 KB | `src/shared/components/ui/rich-text-editor.tsx:5-8`、`src/modules/exams/editor/exam-rich-editor.tsx:4-6`、`src/modules/lesson-preparation/components/blocks/rich-text-block.tsx:3-6` |
| 3 | Bundle | `next.config.ts` 未启用 `experimental.optimizePackageImports`(lucide-recharts-radix-tiptap 全部未优化) | `next.config.ts:6-11` |
| 4 | Bundle | `serverExternalPackages` 仅含 mysql2,未追加 `tencentcloud-sdk-nodejs`(2-3 MB 误打包风险) | `next.config.ts:10` |
| 5 | Bundle | `src/modules/exams/editor/index.ts` barrel 已实际造成构建错误(代码中已有显式绕过注释) | `src/modules/exams/editor/index.ts` |
| 6 | CWV | 无 Web Vitals 上报闭环,生产环境零真实用户性能数据 | `src/app/layout.tsx`(缺失 `useReportWebVitals`) |
| 7 | 组件 | React Compiler(`babel-plugin-react-compiler`)未安装,memoization 缺失无法自动补全 | `package.json` + `next.config.ts` |
| 8 | 组件 | `SidebarProvider` value 内联对象 + toggleSidebar 未 useCallback + resize 监听未防抖,全站 Header/Sidebar 高频重渲染 | `src/modules/layout/components/sidebar-provider.tsx:63-66` |
| 9 | 组件 | 备课编辑器 `LessonPlanEditor` 通过 `useLessonPlanEditor()` 订阅全 store,每次按键触发三栏全量重渲染 | `src/modules/lesson-preparation/components/lesson-plan-editor.tsx:62` |
| 10 | 组件 | 题库表 `QuestionDataTable` 无虚拟化,1000+ 题时首屏渲染秒级卡顿 | `src/modules/questions/components/question-data-table.tsx:45-59` |
| 11 | 数据层 | `copyCoursePlanToClasses` 双层循环单条 INSERT,无事务无批量无大小限制 | `src/modules/course-plans/data-access.ts:347-406` |
| 12 | 数据层 | 跨请求数据缓存完全缺失(0 处 `unstable_cache`),低变更频率数据每次重查 DB | 全项目 |
| 13 | 数据层 | 每次 Server Action 调用必查 DB 解析 DataScope(admin 0 次 / teacher 2 次 / parent 2 次串行) | `src/shared/lib/auth-guard.ts:44-47` |
| 14 | 数据层 | `searchQuestions` JSON 字段 CAST 后 `LIKE %kw%` 全表扫描 | `src/modules/search/data-access.ts:74-75` |
| 15 | 数据层 | `getAllUserIds` 无 LIMIT 全表返回,5000+ 用户场景公告 fan-out 内存爆炸 | `src/modules/users/data-access.ts:348-351` |
### P1 高优先级问题(影响高频接口性能)
详见各分章节,共 9 项关键 P1(含:recharts barrel import、tencentcloud-sdk serverExternalPackages、exams/editor barrel 拆分、loading.tsx 缺失、force-dynamic 滥用、Lighthouse CI 缺失、layout 串行 await、autoGradeSubmission 循环 UPDATE、exams 索引缺失等)。
### 关键优化杠杆点(ROI 排序)
1. **React Compiler 启用**:单点改动,自动解决第 3 部分列出的多数 memoization 反模式
2. **next.config.ts 完善**:单点改动,optimizePackageImports + serverExternalPackages 扩展
3. **tiptap / ReactFlow 动态导入**:3 处文件改动,编辑器路由 chunk 减少 ~300 KB
4. **备课编辑器 store 订阅拆分**:单文件改动,编辑器按键响应速度预计提升 5-10x
5. **unstable_cache 引入**:DataScope + 低变更数据缓存,单次 Server Action DB 查询数减半
---
## 第一部分:Bundle 体积预算审计(首期重点)
### 1.1 大体积依赖清单
| 依赖 | 版本 | 估算体积(min+gzip) | 用途 | 服务端/客户端 | 严重度 |
|------|------|----------------------|------|---------------|--------|
| `recharts` | ^3.6.0 | ~95-110 KB | 图表库(13 处使用) | 客户端 | P1 |
| `@xyflow/react` | ^12.11.0 | ~120-140 KB | ReactFlow 知识图谱 | 客户端 | P1 |
| `react-force-graph-2d` | ^1.29.1 | ~80-100 KB(含 d3-force) | 引力图谱 | 客户端 | P2(已 dynamic) |
| `tencentcloud-sdk-nodejs` | ^4.1.254 | **~2-3 MB**(巨型 SDK) | 腾讯云短信 | 服务端 | P0 |
| `exceljs` | ^4.4.0 | ~250-300 KB | Excel 导入导出 | 服务端 | P1 |
| `@tiptap/*`(5 包 + tiptap-markdown) | 3.x | ~180-220 KB | 富文本编辑器(3 处) | 客户端 | P0 |
| `@dnd-kit/*`(core+sortable+utilities) | 6.x/10.x | ~30-40 KB | 拖拽 | 客户端 | P2 |
| `lucide-react` | ^0.562.0 | 单图标 ~1 KB,全集 ~800 KB | 图标库(100+ 处) | 客户端 | P1 |
| `radix-ui/*`(16 个包) | 各版本 | 单包 ~3-8 KB | UI 原语 | 客户端 | P2 |
| `openai` | ^6.25.0 | ~60-80 KB | AI SDK | 服务端 | P2 |
| `react-markdown` + remark-* + rehype-* | 4.x-10.x | ~50-70 KB | Markdown 渲染 | 客户端 | P2 |
| `@tanstack/react-query` + `react-table` | 5.x/8.x | ~35-45 KB | 数据层 | 客户端 | P3 |
### 1.2 "use client" 指令分布
| 指标 | 数值 |
|------|------|
| `"use client"` 文件总数 | **553** |
| `src/**/*.{ts,tsx}` 文件总数 | >700 |
| 客户端组件占比(估算) | **~70-75%** |
| `shared/components/ui/` 下 "use client" 文件 | 40 个 |
**典型反例**:
| 文件 | 问题 | 严重度 |
|------|------|--------|
| `src/shared/components/ui/status-badge.tsx:1` | 仅渲染 Badge 无 hook 无事件,过度标记为 client | P2 |
| `src/shared/components/ui/label.tsx:1` | 仅 forwardRef + cva,可考虑改 server | P3 |
### 1.3 动态导入使用情况
| 模式 | 数量 | 位置 |
|------|------|------|
| `next/dynamic` | **2** | `force-graph.tsx:18`、`homework-assignment-exam-error-explorer-lazy.tsx:5` |
| `React.lazy` | **0** | 无 |
| 服务端动态 `import()` | 1 | `sms-channel.ts:186`(tencentcloud) |
**应使用但未使用动态导入的重型组件**:
#### [P0] ReactFlow(@xyflow/react)未动态导入
- 位置:`src/modules/textbooks/components/knowledge-graph.tsx:5-14`
- 代码:`import { ReactFlow, Background, ... } from "@xyflow/react"` 顶层静态导入
- 影响路由:`teacher/textbooks/[id]` 与 `student/learning/textbooks/[id]`
- 估算影响:每个 textbook 路由 chunk 增加 ~120-140 KB
- 修复参考:`force-graph.tsx:74-80` 已有的 `dynamic(() => import(...), { ssr: false, loading: ... })` 模式
#### [P0] tiptap 富文本编辑器未动态导入(3 处)
- 位置 1:`src/shared/components/ui/rich-text-editor.tsx:5-8`
- 位置 2:`src/modules/exams/editor/exam-rich-editor.tsx:4-6`
- 位置 3:`src/modules/lesson-preparation/components/blocks/rich-text-block.tsx:3-6`
- 影响路由:`teacher/exams/[id]/edit-rich`、`teacher/exams/new`、教材阅读页链路、备课编辑页
- 估算影响:每个含编辑器路由增加 ~180-220 KB
### 1.4 大体积第三方库使用模式
#### lucide-react(100+ 处使用)
- ✅ 全部使用具名导入 `import { IconName } from "lucide-react"`
- ❌ 缺少 `experimental.optimizePackageImports: ["lucide-react"]`(Turbopack 内建 tree-shaking 可能不彻底)
- 严重度:P1
#### recharts(13 处使用)
- ✅ 业务侧具名导入
- ❌ [P1 反例] `src/shared/components/ui/chart.tsx:5` → `import * as RechartsPrimitive from "recharts"` barrel 导入
- 影响:chart.tsx 被 12 个图表组件引用,barrel 导入让 Turbopack 难以精确剔除未用子模块
- 修复:改为按需具名导入
#### @xyflow/react(4 处使用,仅 textbooks 模块)
- 模块隔离良好 ✅
- 但未动态导入导致 textbook 路由 chunk 膨胀 ❌
#### tencentcloud-sdk-nodejs(服务端专用,已正确隔离)
- 位置:`src/modules/notifications/channels/sms-channel.ts:186`
- 模式:`await import("tencentcloud-sdk-nodejs")` ✅
- [P0] 建议追加 `serverExternalPackages: ["tencentcloud-sdk-nodejs"]` 防止误打包
#### exceljs(1 处使用)
- 位置:`src/shared/lib/excel.ts:3`
- 模式:静态 `import ExcelJS from "exceljs"`(值导入)
- 风险:若被 client 组件误引用将导致 250-300 KB 打包
### 1.5 next.config.ts 配置审计
**当前完整内容**(11 行):
```ts
const nextConfig: NextConfig = {
output: "standalone",
serverExternalPackages: ["mysql2"],
};
```
**缺失配置项**:
| 缺失配置 | 影响 | 严重度 |
|---------|------|--------|
| `experimental.optimizePackageImports` | lucide-react/recharts/@xyflow/react/@radix-ui/* /@tiptap/* 未启用按需优化 | P0 |
| `serverExternalPackages` 未含 `tencentcloud-sdk-nodejs` | 2-3 MB SDK 误打包风险 | P0 |
| `serverExternalPackages` 未含 `exceljs` | 250-300 KB 误打包风险 | P1 |
| 自定义 webpack/turbopack manualChunks | 无 splitChunks 自定义 | P2 |
| `experimental.webVitalsAttribution` | 无法精确定位 Web Vitals 来源元素 | P2 |
**建议补充配置**:
```ts
const nextConfig: NextConfig = {
output: "standalone",
serverExternalPackages: ["mysql2", "tencentcloud-sdk-nodejs", "exceljs"],
experimental: {
optimizePackageImports: [
"lucide-react",
"recharts",
"@xyflow/react",
"@tiptap/react",
"@tiptap/starter-kit",
"@tiptap/extension-placeholder",
"@tiptap/extension-image",
"@radix-ui/react-dialog",
"@radix-ui/react-dropdown-menu",
"@radix-ui/react-select",
// ... 其余 radix 包
],
},
};
```
### 1.6 Barrel File 风险
| 路径 | 导出 | 风险 |
|------|------|------|
| `src/shared/components/ui/` | 无 index.ts barrel | ✅ 良好 |
| `src/modules/exams/editor/index.ts` | ExamRichEditor + 6 个 tiptap 扩展 + 类型 | **P0 高风险** |
| `src/modules/exams/ai-pipeline/index.ts` | — | 低 |
| `src/shared/hooks/index.ts` | 7 个 hook | 低 |
**[P0] exams/editor/index.ts 高风险 barrel**
- 位置:`src/modules/exams/editor/index.ts:1-26`
- 已造成实际问题:`src/app/(dashboard)/teacher/exams/[id]/edit-rich/page.tsx:9-14` 已有显式注释绕过 barrel
- 但同文件第 15 行同步导入 `ExamRichForm`(其内部第 22-32 行又通过 barrel 导入)→ 绕过失效
- 错误信息:`'Class extends value undefined is not a constructor or null'`(Server Component 连带加载 @tiptap/react 客户端扩展)
- 修复方向:拆分为 `editor/types.ts`(仅类型,可被 server 引用)+ `editor/index.ts`(仅值导出,禁止 server 引用)
### 1.7 图片/字体/静态资源
| 资源 | 现状 | 评估 |
|------|------|------|
| `next/image` 使用 | 4 处(exam-preview、scan-uploader、scan-image-viewer、exam-rich-form) | ✅ 良好 |
| 原生 `
` | 1 处(scan-image-viewer.tsx:143,已加 eslint-disable 注释) | ⚠️ 见 CLS-1 |
| `next/font` | 1 个(Inter,`display: "swap"` + `variable`) | ✅ 良好 |
| `manifest.ts` | 完整 | ✅ 良好 |
| `public/` | 5 个 SVG | ✅ 良好 |
### 1.8 Bundle 预算建议基线表
> 因当前 `next build` 失败无法获取实际产物体积,以下为基于依赖估算的 K12 教务系统推荐基线(gzip 后)。
| 路由类别 | First Load JS 阈值 | 当前估算 | 严重度 |
|---------|-------------------|---------|--------|
| 落地页 / 登录 / 注册(`(auth)/*`) | ≤ 180 KB | ~150-180 KB | P2 |
| 学生端 Dashboard | ≤ 250 KB | ~250-320 KB | P2 |
| 教师端 Dashboard | ≤ 280 KB | ~280-350 KB | P2 |
| 列表/详情页(公告/作业/成绩等) | ≤ 300 KB | ~300-400 KB(recharts 拉入) | P1 |
| **教材阅读页**(含 ReactFlow + tiptap + force-graph) | ≤ 600 KB | **~700-900 KB**(全量静态导入) | **P0** |
| **试卷富文本编辑页**(含 tiptap 全家桶) | ≤ 500 KB | **~600-750 KB** | **P0** |
| **备课编辑页**(含 tiptap + dnd-kit) | ≤ 550 KB | ~600-800 KB | **P0** |
| 单路由增量 chunk(动态加载组件) | ≤ 200 KB | force-graph 单独 chunk ~80 KB ✅ | — |
| 共享框架 chunk(react/react-dom/next) | ≤ 130 KB | ~130 KB(标准) | — |
| 单文件体积上限(lint 规则) | ≤ 1000 行 | 已有项目规则约束 | — |
---
## 第二部分:Core Web Vitals 审计
### 2.1 LCP(最大内容绘制)
#### [P1] LCP-1:根 layout.tsx 串行 await
- **位置**:`src/app/layout.tsx:45-50`
- **问题**:每个 SSR 请求串行调用 `getLocale()` → `getMessages()` → `auth()`,三者无数据依赖
- **影响**:TTFB、LCP、FCP(每个页面渲染都被推迟)
- **修复**:`const [locale, messages, session] = await Promise.all([getLocale(), getMessages(), auth()])`
#### [P1] LCP-3:teacher/student/parent/management 根路由缺失 loading.tsx
- **问题**:通过 Glob 搜索 `**/loading.tsx` 仅找到 `admin/loading.tsx`,4 个角色根路由段下无 loading.tsx
- **影响**:角色切换/首次进入时白屏闪烁,LCP/CLS 退化
- **建议**:为 4 个角色根路由分别创建 `loading.tsx`,复用 `DashboardLoadingSkeleton`
#### [P1] LCP-5:force-dynamic 滥用,无 ISR 缓存收益
- **问题**:全项目共 **96 处** `export const dynamic = "force-dynamic"`,即使是相对静态的页面(学校列表、部门列表、教材目录)也强制动态
- **影响**:每请求都重新 SSR + DB 查询,TTFB/LCP 无静态缓存命中加速
- **建议**:对低频变更页面用 `revalidate = 3600` 替代 `force-dynamic`
#### [P2] LCP-2:备课编辑器 textbook → chapters 数据瀑布
- **位置**:`src/app/(dashboard)/teacher/lesson-plans/[planId]/edit/page.tsx:34-46`
- **问题**:第 34-46 行串行调用 `getTextbookById()` → `getChaptersByTextbookId()`
- **参考**:`parent/lesson-plans/[planId]/view/page.tsx:49-52` 已修复为 `Promise.all`
#### 正面发现
- ✅ `teacher/lesson-plans/page.tsx`、`teacher/exams/[id]/build/page.tsx`、`teacher/exams/[id]/analytics/page.tsx`、`parent/lesson-plans/[planId]/view/page.tsx` 均用 `Promise.all`
- ✅ 4 个角色 dashboard 均用 React `use()` + Suspense 流式渲染
- ✅ `DashboardSection` 封装 `SectionErrorBoundary + Suspense + 骨架屏`,分区流式渲染
### 2.2 CLS(累计布局偏移)
#### [P2] CLS-1:scan-image-viewer.tsx 使用 `
` 且未指定宽高
- **位置**:`src/modules/homework/components/scan-image-viewer.tsx:142-148`
- **问题**:原生 `
`(已加 eslint-disable 注释)仅设 `maxHeight: "80vh"`,无 width/height 也无 aspect-ratio
- **影响**:图片加载完成时推动布局
- **修复**:用 `next/image` 替换(同文件第 194-200 行缩略图已正确用 `next/image fill sizes="48px"`),或为 `
` 加 `aspect-ratio` 内联样式
#### 正面发现
- ✅ 字体配置正确(`display: "swap"` + `variable`)
- ✅ `