Files
NextEdu/docs/architecture/audit/performance-budget-audit-report.md
SpecialX 9ce8d6d3fd refactor(data-access): 补全剩余 37 个 data-access 文件迁移至 cacheFn
迁移范围:lesson-preparation 11 文件、attendance 3 文件、settings 4 文件、classes-invitations、跨模块接口 4 文件、adaptive-practice/ai/onboarding/invitation-codes/search/standards/scheduling/leave-requests/audit。修复 3 个 cacheFn 块位置错误。同步架构文档迁移范围至 83 文件 250+ 函数。
2026-07-06 15:12:30 +08:00

1176 lines
65 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 性能预算重构专项 - 完整审计报告
> **审计日期**: 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 解析 DataScopeadmin 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 项关键 P1recharts 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-react100+ 处使用)
- ✅ 全部使用具名导入 `import { IconName } from "lucide-react"`
- ❌ 缺少 `experimental.optimizePackageImports: ["lucide-react"]`Turbopack 内建 tree-shaking 可能不彻底)
- 严重度P1
#### recharts13 处使用)
- ✅ 业务侧具名导入
- ❌ [P1 反例] `src/shared/components/ui/chart.tsx:5``import * as RechartsPrimitive from "recharts"` barrel 导入
- 影响chart.tsx 被 12 个图表组件引用barrel 导入让 Turbopack 难以精确剔除未用子模块
- 修复:改为按需具名导入
#### @xyflow/react4 处使用,仅 textbooks 模块)
- 模块隔离良好 ✅
- 但未动态导入导致 textbook 路由 chunk 膨胀 ❌
#### tencentcloud-sdk-nodejs服务端专用已正确隔离
- 位置:`src/modules/notifications/channels/sms-channel.ts:186`
- 模式:`await import("tencentcloud-sdk-nodejs")`
- [P0] 建议追加 `serverExternalPackages: ["tencentcloud-sdk-nodejs"]` 防止误打包
#### exceljs1 处使用)
- 位置:`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 | ✅ 良好 |
| 原生 `<img>` | 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 KBrecharts 拉入) | 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 ✅ | — |
| 共享框架 chunkreact/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-3teacher/student/parent/management 根路由缺失 loading.tsx
- **问题**:通过 Glob 搜索 `**/loading.tsx` 仅找到 `admin/loading.tsx`4 个角色根路由段下无 loading.tsx
- **影响**:角色切换/首次进入时白屏闪烁LCP/CLS 退化
- **建议**:为 4 个角色根路由分别创建 `loading.tsx`,复用 `DashboardLoadingSkeleton`
#### [P1] LCP-5force-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-1scan-image-viewer.tsx 使用 `<img>` 且未指定宽高
- **位置**`src/modules/homework/components/scan-image-viewer.tsx:142-148`
- **问题**:原生 `<img>`(已加 eslint-disable 注释)仅设 `maxHeight: "80vh"`,无 width/height 也无 aspect-ratio
- **影响**:图片加载完成时推动布局
- **修复**:用 `next/image` 替换(同文件第 194-200 行缩略图已正确用 `next/image fill sizes="48px"`),或为 `<img>``aspect-ratio` 内联样式
#### 正面发现
- ✅ 字体配置正确(`display: "swap"` + `variable`
-`<iframe>` 全项目零使用
- ✅ Sonner Toaster 使用 fixed 定位
- ✅ ThemeProvider 配置 `disableTransitionOnChange`
### 2.3 INP交互到下一次绘制
#### [P2] INP-3AiAssistantWidget 未懒加载
- **位置**`src/app/(dashboard)/layout.tsx:7,29`
- **问题**`<AiAssistantWidget />` 在 dashboard layout 同步渲染,依赖 `useFloatingBall`(拖拽位置计算 + localStorage 持久化 + 屏幕边缘吸附)+ `AiChatPanel` + `Sheet`,未通过 `next/dynamic` 懒加载
- **影响**INP主线程占用、FCPhydration 时间)
- **建议**:用 `next/dynamic` 包裹,`ssr: false`,初始仅渲染悬浮球,点击后再加载 chat panel
#### [P3] INP-4AppSidebar 未 memo 化导航项
- **位置**`src/modules/layout/components/app-sidebar.tsx:31-60`
- **问题**:每次渲染对 `allNavItems``filter + map` 重新计算 `navItems`,未使用 `useMemo`
- **建议**`useMemo` 包裹 `navItems` 计算,依赖 `[allNavItems, permissions]`
#### 正面发现
- ✅ 全项目 44 处 `useTransition` / `useDeferredValue` 使用覆盖良好exam-assembly 防抖、exam-rich-form 双 transition、scan-uploader 等)
- ✅ 备课编辑器使用 zustand store无大对象 setState
- ✅ react-force-graph-2d 已正确动态加载
### 2.4 FCP / TTFB
#### [P1] FCP-1根 layout 串行 await同 LCP-1
见 LCP-1。
#### [P2] FCP-4next.config.ts 未启用 optimizePackageImports
见第一部分 1.5。
#### 正面发现
-`proxy.ts` 中间件无数据库同步调用,纯 JWT 位图运算
-`instrumentation.ts` 启动时一次性执行
-`globals.css` 多层 @import 均为本地资源无外部阻塞
### 2.5 资源加载
#### [P2] RES-1metadata 缺 metadataBase / openGraph / robots
- **位置**`src/app/layout.tsx:15-24`
- **问题**metadata 仅含 `title`/`description`/`manifest`/`appleWebApp`,缺 `metadataBase``openGraph``twitter``robots``src/app/icon.svg` 文件可能不存在(`public/` 下仅有 5 个 SVG
- **影响**FCPOG image 加载、SEO、build 时 warn
- **建议**:补充 `metadataBase: new URL(process.env.NEXTAUTH_URL ?? "http://localhost:3000")`
#### 正面发现
-`viewport` 导出符合 Next.js 16 规范
-`manifest.ts` 配置完整
-`<Script>` 标签零使用,无第三方脚本阻塞
-`next/image` 使用合规4 处)
### 2.6 性能监控缺失
#### [P0] MON-1无 Web Vitals 上报闭环
- **问题**Grep `web-vitals|reportWebVitals|useReportWebVitals|onCLS|onINP|onLCP``src/` 下零匹配;`package.json``web-vitals` 依赖
- **影响**:生产环境无法追踪真实用户 LCP/CLS/INP/TTFB/FCP所有性能问题只能靠经验推断
- **建议**
1. 安装 `web-vitals`
2.`src/app/layout.tsx` 或新建 `src/instrumentation-client.ts` 实现 `useReportWebVitals`,上报到 `/api/web-vitals` 或第三方Vercel Analytics / PostHog / Sentry
#### [P1] MON-2无 Lighthouse CI 配置
- **问题**:项目根目录无 `.lighthouserc.json` / `.lighthouserc.js``package.json``@lh3/cli` 依赖;`.gitea/workflows/ci.yml` 无 lighthouse step
- **影响**CI 流水线无 Web Vitals 回归门槛,性能劣化无法自动拦截
- **建议**:在 `.gitea/workflows/` 新增 `lighthouse.yml`,配置 Lighthouse CI 对关键路由(`/dashboard``/teacher/lesson-plans``/admin/questions`)做采样断言
#### [P2] MON-4Playwright 无性能断言
- **问题**`playwright.config.ts` 已配置 E2E + 视觉回归,但未使用 `page.metrics()``@playwright/test-performance` 做性能断言
- **建议**:在 `tests/e2e/` 新增 `performance.spec.ts`,使用 `page.evaluate(() => performance.getEntriesByType('navigation'))` 断言关键路由 LCP
### 2.7 路由级性能风险
| 路由 | LCP 风险 | INP 风险 | 严重度 |
|------|---------|---------|--------|
| 备课编辑器 `teacher/lesson-plans/[planId]/edit` | 中waterfall + ReactFlow + tiptap | 中(多 useTransition + zustand | P2 |
| 试卷富文本编辑器 `teacher/exams/[id]/edit-rich` | 中(两 await 无 Promise.all | 高tiptap + 多状态) | P2 |
| 教材详情/知识图谱 `teacher/textbooks/[id]` | 低(已 Promise.all | 高react-force-graph + @xyflow | P2 |
| 题库管理 `admin/questions` | 中200 题一次拉) | 高200 题列表渲染) | P2 |
| 4 个角色 dashboard | 低(已流式渲染) | 中recharts | P3 |
### 2.8 Web Vitals 预算建议表
| 指标 | Good达标| Needs Improvement | Poor | 建议预算P95|
|------|------------|-------------------|------|---------------|
| **LCP** | ≤ 2.5s | 2.5s - 4.0s | > 4.0s | ≤ 2.5s(备课编辑器允许 ≤ 3.0s|
| **INP** | ≤ 200ms | 200ms - 500ms | > 500ms | ≤ 200ms仪表盘图表允许 ≤ 300ms|
| **CLS** | ≤ 0.1 | 0.1 - 0.25 | > 0.25 | ≤ 0.05 |
| **TTFB** | ≤ 800ms | 800ms - 1800ms | > 1800ms | ≤ 800msdashboard 路由)|
| **FCP** | ≤ 1.8s | 1.8s - 3.0s | > 3.0s | ≤ 1.8s |
#### 按路由细分预算建议
| 路由 | LCP | INP | TTFB | 主要风险 |
|------|-----|-----|------|---------|
| `/login` | ≤ 1.5s | ≤ 100ms | ≤ 500ms | 无 |
| `/admin/dashboard` | ≤ 2.5s | ≤ 300ms | ≤ 800ms | recharts 多图表 + 6 个并行数据流 |
| `/teacher/dashboard` | ≤ 2.5s | ≤ 300ms | ≤ 800ms | 同上 |
| `/student/dashboard` | ≤ 2.0s | ≤ 200ms | ≤ 800ms | 数据较少 |
| `/parent/dashboard` | ≤ 2.5s | ≤ 250ms | ≤ 800ms | 多子女卡片 + AI 摘要 |
| `/teacher/lesson-plans/[id]/edit` | ≤ 3.0s | ≤ 400ms | ≤ 1000ms | ReactFlow + tiptap + 数据瀑布 |
| `/teacher/exams/[id]/edit-rich` | ≤ 3.0s | ≤ 400ms | ≤ 1000ms | tiptap 富文本 |
| `/teacher/textbooks/[id]` | ≤ 2.5s | ≤ 400ms | ≤ 800ms | react-force-graph-2d动态加载|
| `/admin/questions` | ≤ 2.5s | ≤ 400ms | ≤ 800ms | 200 题列表 |
---
## 第三部分:组件渲染性能审计
### 3.1 Memoization 缺失审计
#### 全局使用统计
| 指标 | 数量 | 备注 |
|------|------|------|
| `React.memo` / `memo()` | **4 处 / 4 文件** | 严重偏低(项目有 100+ 客户端组件) |
| `useMemo` | 113 处 / 70 文件 | 中等覆盖 |
| `useCallback` | 129 处 / 53 文件 | 中等覆盖 |
| `style={{ ... }}` 内联 | **122 处 / 36 文件** | 大量内联对象每次渲染重建 |
`memo / useMemo` 比例 = 4 / 113 ≈ **3.5%**,远低于企业级项目 30%+ 常见水平。
#### React.memo 实际使用位置(仅 4 处)
- `src/modules/ai/components/ai-markdown-renderer.tsx:162``AiMarkdownRenderer` 正确 memo
- `src/modules/textbooks/components/graph-prerequisite-edge.tsx`
- `src/modules/textbooks/components/graph-kp-node.tsx`
- `src/modules/student/components/student-courses-view.tsx`
#### [P1] 反例 1recharts 内联对象 props
- **位置**`src/modules/dashboard/components/admin-dashboard/user-growth-chart.tsx:39-61`
- **代码**
```tsx
<LineChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
<XAxis tick={{ fontSize: 12 }} />
<Tooltip contentStyle={{ backgroundColor: "hsl(var(--background))", ... }} />
<Line dot={{ fill: "hsl(var(--primary))", r: 3 }} />
</LineChart>
```
- **影响**`margin` / `tick` / `contentStyle` / `dot` 每次渲染均为新对象引用,触发 recharts 内部 SVG 元素重渲染
- **修复**:提升为模块级常量
#### [P1] 反例 2内联事件回调每行重建
- **位置**`src/modules/messaging/components/message-list.tsx:319-321`
- **代码**`<button onClick={(e) => void handleToggleStar(e, m.id, isStarred)} ... />`
- **影响**20 条消息 × 多个内联回调,父组件每次 setTab/setKeyword 都重建 20+ 函数
#### [P2] 反例 3style 内联 122 处
- **位置**`lesson-preparation/components/detail-panel/qa-editor.tsx`16 处)、`paper-editor/paper-context-menu.tsx`11 处)、`paper-editor/inline-node.tsx`11 处)、`detail-panel/detail-head.tsx`7 处)、`detail-panel/detail-panel.tsx`7 处)
- **影响**:备课编辑器子组件每次父级渲染都重算样式对象
#### [P2] 反例 4昂贵计算未 memo
- **位置**`src/modules/lesson-preparation/components/paper-editor/paper-editor.tsx:36`
- **代码**`const textbookNode = doc.nodes.find((n) => n.type === "textbook_content");`
- **影响**`doc` 引用变化(任意节点更新)都重算
- **同样问题**`src/modules/lesson-preparation/components/structure-tree/structure-tree.tsx:24`
### 3.2 Context 嵌套审计
#### 全部 Context Provider 清单(共 17 个)
根布局链路providers.tsx + dashboard layout.tsx
```
QueryClientProvider (1)
└─ NextIntlClientProvider (2)
└─ ThemeProvider (3)
└─ SessionProvider (4)
└─ NuqsAdapter (5)
└─ AiClientProvider (6, dashboard 层)
└─ SidebarProvider (7, dashboard 层)
└─ children
```
**7 层嵌套**,未达到 5+ 警戒线的"严重"标准但有提升空间。
#### [P0] 关键反例SidebarProvider value 内联对象
- **位置**`src/modules/layout/components/sidebar-provider.tsx:63-66`
- **代码**
```tsx
<SidebarContext.Provider
value={{ expanded, setExpanded, isMobile, toggleSidebar }}
>
```
- **问题**
1. `value` 是每次渲染新建的对象字面量,引用必然不同
2. `toggleSidebar` 不是 `useCallback`,每次重建
3. `useSidebar()` 的所有 consumer`SiteHeader`、`AppSidebar` 等)在 SidebarProvider 任意 state 变化时全部重渲染
4. 第 51 行 `window.addEventListener("resize", checkMobile)` 未防抖,拖动窗口时连续触发 `setIsMobile`
- **影响**:全站所有 dashboard 页面 Header/Sidebar 重渲染频率高
- **修复**`value` 用 `useMemo([expanded, isMobile])``toggleSidebar` 用 `useCallback`resize 加防抖
### 3.3 大列表虚拟化审计
#### 关键发现package.json 中未安装任何虚拟化库
- ❌ `@tanstack/react-virtual`
- ❌ `react-window`
- ❌ `react-virtuoso`
#### [P0] 重点反例:题库表无虚拟化
- **位置**`src/modules/questions/components/question-data-table.tsx:45-59`
- **代码**
```tsx
const table = useReactTable({
data, // 题库可能上千条
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(), // 客户端全量过滤
// 无 enableRowVirtualization / getVirtualizedRowModel
})
```
- **影响**:题库 1000+ 条时,即使分页每页 10 条,`getFilteredRowModel` 仍要遍历全部数据;切换筛选条件时一次性 diff 大量 DOM
- **预期数据量**1000-10000+ 题
#### 其他大列表
| 列表 | 文件:行号 | 渲染方式 | 预期数据量 | 严重度 |
|------|----------|----------|-----------|--------|
| 考试列表 | `src/modules/exams/components/exam-data-table.tsx:78-87` | 全量渲染 | 100-1000+ | P1 |
| 出勤表 | `src/modules/attendance/components/attendance-sheet.tsx` | 一次性渲染全班学生 × 6 状态按钮 | 30-60 学生/班 × 6 = 180-360 控件 | P1 |
| 审计日志表 | `src/modules/audit/components/audit-log-table.tsx:61-105` | 服务端分页 + 每行 `AuditLogDetailDialog` | 10-50/页 | P2 |
| 消息列表 | `src/modules/messaging/components/message-list.tsx:263-352` | 客户端分页 PAGE_SIZE=20每行 Card 较重 | 20/页 | P2 |
| 用户列表 | `src/modules/users/components/admin-users-view.tsx:219-265` | 服务端分页 + 每行 DropdownMenu | 10-50/页 | P2 |
| 备课结构树 | `src/modules/lesson-preparation/components/structure-tree/structure-tree.tsx:79-107` | 全量渲染所有节点 | 5-50 节点 | P3 |
| 教材章节图 | `src/modules/textbooks/components/knowledge-graph.tsx` + `force-graph.tsx` | @xyflow/react + react-force-graph-2d | 100+ 节点 | P2 |
### 3.4 Zustand Store 性能审计
#### Store 清单(共 2 个)
| Store | 文件 | slices | 使用情况 |
|-------|------|--------|----------|
| `useUiStore` | `src/shared/stores/ui-store.ts` | 单一 | **未被任何组件使用(死代码)** |
| `useLessonPlanEditor` | `src/modules/lesson-preparation/hooks/use-lesson-plan-editor.ts` | 5 个 sliceseditor / selection / version / history / expanded | 备课编辑器全量使用 |
#### [P1] 反例 1useModal 返回新对象引用(当前为死代码)
- **位置**`src/shared/stores/ui-store.ts:73-78`
- **代码**
```tsx
return useUiStore((s) => ({
open: Boolean(s.modals[name]),
props: s.modals[name]?.props,
openModal: (props?: ModalProps) => s.openModal(name, props),
closeModal: () => s.closeModal(name),
}))
```
- **问题**Zustand v5 默认使用 `Object.is` 比较selector 返回新对象字面量意味着每次 store 变化都触发组件重渲染
- **当前状态**Grep 显示 `useUiStore` / `useModal` 仅在 `ui-store.ts` 自身出现,**项目其他位置未使用**,是预备的 modal registry 但未落地。一旦采用会立刻触发性能问题
- **修复**:用 `useShallow` 包裹
#### [P0] 反例 2备课编辑器无 selector 订阅全 store
- **位置**4 处直接调用 `useLessonPlanEditor()` 不带 selector
| 文件:行号 | 解构 | 影响 |
|----------|------|------|
| `src/modules/lesson-preparation/components/lesson-plan-editor.tsx:62` | `const editor = useLessonPlanEditor()` | 订阅全 store每次按键/选区/展开/撤销/保存状态变化都重渲染整个三栏布局 |
| `src/modules/lesson-preparation/components/blocks/text-study-block.tsx:22` | `const { updateNode } = useLessonPlanEditor()` | updateNode 引用稳定,但订阅粒度过粗 |
| `src/modules/lesson-preparation/components/blocks/exercise-block.tsx:31` | `const { updateNode, planId } = useLessonPlanEditor()` | 同上 |
| `src/modules/lesson-preparation/components/paper-editor/paper-context-menu.tsx:40` | 多重解构 | 同上 |
- **影响**:备课编辑器是高频交互场景(键盘输入 + 节点选中 + 撤销重做),订阅全 store 导致每次按键都重渲染 StructureTree + PaperEditor + DetailPanel 三个子树
- **修复**:拆为多个细粒度 selector`useLessonPlanEditor((s) => s.title)`、`s.isSaving`、`s.isDirty` 等block 组件改为 `const updateNode = useLessonPlanEditor((s) => s.updateNode)`
#### 正面发现
- ✅ `useLessonPlanEditor` 已正确拆分为 5 个 slice便于按 slice 订阅
- ✅ Slice 文件使用 `import type { StateCreator }` 类型导入,无运行时循环依赖
- ✅ 备课模块其余 25 处 `useLessonPlanEditor((s) => s.xxx)` 使用了正确 selector 模式
### 3.5 Server / Client 边界审计
#### 优秀实践
- ✅ Server Component 路由分发:`src/app/(dashboard)/dashboard/page.tsx`
- ✅ Server Component + Suspense + Streamsadmin-dashboard.tsx
- ✅ Server Component 嵌套 Providerdashboard layout 在 server 函数体内 `createFullAiClientService()`,避免模块级单例
- ✅ 100+ 页面 Server Component
- ✅ Server → Client 通过 ReactNode slot`<SidebarProvider sidebar={<AppSidebar />}>`
- ✅ Server Action + `useFormStatus`5 处
- ✅ Dialog 内部状态管理lesson-preparation providers 用 `useMemo` 包裹 service value
#### [P2] 反例Server → Client 数据传递过大对象
| 传递点 | 文件 | 数据量 | 评估 |
|--------|------|--------|------|
| `messages` prop → `MessageList` | `src/app/(dashboard)/messages/page.tsx` → `message-list.tsx` | 全部 messages含 content、senderName 等) | ⚠️ 可能过大,无字段裁剪 |
| `messages` prop → NextIntlClientProvider | `providers.tsx:46` | **全量 i18n 字典** | ⚠️ 应按 namespace 拆分 |
### 3.6 React 19 特定优化审计
#### 关键缺失
| 特性 | 是否启用 | 严重度 | 备注 |
|------|---------|--------|------|
| React Compiler (`babel-plugin-react-compiler`) | ❌ **未安装** | **P0** | package.json 无依赖next.config.ts 无配置 |
| `use()` Hook 处理 Promise | ❌ 0 处使用 | P2 | admin-dashboard 注释声称使用,实际是 Suspense + await |
| `useOptimistic` | ❌ 0 处使用 | P1 | 消息星标、出勤状态、批改提交等多处应使用 |
| `useFormStatus` | ✅ 5 处 | — | attendance-sheet、attendance-rules-form、create-chapter-dialog、textbook-form-dialog、password-change-form 等 |
| `useActionState` | ✅ 3 处 | — | password-change-form、leave-request-form、leave-review-dialog |
#### [P0] React Compiler 缺失
- **问题**
- `package.json` 无 `babel-plugin-react-compiler` 依赖
- `next.config.ts` 未配置 `reactCompiler: true`
- `eslint-plugin-react-compiler` 未安装
- **影响**React Compiler 可自动 memoize 组件内部状态和派生值,可一次性解决第 3.1 节列出的所有内联对象/未 memo 问题。React 19 + Next.js 16 是启用 Compiler 的最佳时机
- **修复**:安装 `babel-plugin-react-compiler` + `eslint-plugin-react-compiler`,在 `next.config.ts` 加 `experimental: { reactCompiler: true }` 或使用 Next.js 16 的稳定 API
#### [P1] useOptimistic 缺失场景
##### 场景 1消息星标切换
- **位置**`src/modules/messaging/components/message-list.tsx:119-148`
- **现状**:用 `starredOverride` state + try/catch 回滚模拟乐观更新
- **修复**
```tsx
const optimisticMessages = useOptimistic(messages, (state, { id, isStarred }) =>
state.map(m => m.id === id ? { ...m, isStarred } : m)
)
```
##### 场景 2出勤状态切换
- **位置**`src/modules/attendance/components/attendance-sheet.tsx:129-140`
- **现状**`statuses` state 实际是乐观值容器
- **修复**:用 `useOptimistic` 表达
##### 场景 3成绩录入
- **位置**`src/modules/grades/components/batch-grade-entry.tsx`、`grade-record-form.tsx`
### 3.7 重渲染热点审计
#### [P0] 7.1 备课编辑器lesson-preparation— 重灾区
##### 7.1.1 LessonPlanEditor 订阅全 store
- **位置**`src/modules/lesson-preparation/components/lesson-plan-editor.tsx:62`
- **代码**`const editor = useLessonPlanEditor();`
- **影响**:该组件渲染顶部工具栏(标题输入框、保存/撤销/重做/发布等 10+ 按钮)+ 三栏布局StructureTree + PaperEditor + DetailPanel+ 5 个 Dialog。`editor` 任意字段变化(每次按键改 title、每次选区变化、每次撤销重做、每次保存状态切换都触发整个组件树重渲染
- **额外问题**`editor.canUndo()` / `editor.canRedo()` 在 render 中直接调用(第 189-190 行),不是 selector每次渲染都执行函数调用且依赖整个 history slice
##### 7.1.2 PaperEditor 订阅 doc 整体
- **位置**`src/modules/lesson-preparation/components/paper-editor/paper-editor.tsx:25`
- **代码**`const doc = useLessonPlanEditor((s) => s.doc);`
- **影响**`doc` 是不可变对象,任意 node 更新都生成新 `doc` 引用,导致 PaperEditor 全量重渲染
- **额外问题**:第 36 行 `doc.nodes.find()` 无 memo第 54-65 行 `onPaperContextMenu` / `onInlineNodeContextMenu` / 第 86 行 `onContextMenu` 内联回调;第 133-141 行 `expandedNodes.map` 内联函数
##### 7.1.3 StructureTree 同样问题
- **位置**`src/modules/lesson-preparation/components/structure-tree/structure-tree.tsx:12-14`
- **影响**:订阅 `doc` + `expandedNodeIds` + `addNode`,每次 doc 变化都重渲染所有 TreeNodeRow。第 79-107 行 `teachingNodes.map` 内联 `childItems` 数组与 `onToggleChildren` 回调
##### 7.1.4 tree-node-row.tsx 内联 style 11 处
- **位置**`src/modules/lesson-preparation/components/structure-tree/tree-node-row.tsx`
- **影响**11 处 `style={{}}` 内联,每次父级 doc 变化都重算
#### [P1] 7.3 表格组件(@tanstack/react-table
##### 7.3.1 题库表 QuestionDataTableP0
- **位置**`src/modules/questions/components/question-data-table.tsx:36-155`
- **问题**:无虚拟化(见 3.3+ 第 78-93 行 `table.getHeaderGroups().map` 渲染表头 + 第 96-111 行 `table.getRowModel().rows.map` 渲染所有行 + 第 102-108 行 `row.getVisibleCells().map` 渲染所有单元格
- **额外问题**:第 61-64 行 `selectedIds` useMemo 依赖 `[table, getRowId]`table 引用每次 useReactTable 调用都变化,导致 memo 失效
##### 7.3.2 考试表 ExamDataTableP1
- **位置**`src/modules/exams/components/exam-data-table.tsx:33-132`
- **问题**:同样问题,无虚拟化
##### 7.3.3 审计日志表P2
- **位置**`src/modules/audit/components/audit-log-table.tsx:31-118`
- **问题**:服务端分页(好),但每行渲染 `<AuditLogDetailDialog>`(第 101 行),即使未打开也实例化 Dialog 组件
- **建议**:改为点击行时 `next/dynamic` 加载 Dialog
#### [P2] 7.4 表单组件
##### 7.4.2 备课编辑器标题输入直接改 store
- **位置**`src/modules/lesson-preparation/components/lesson-plan-editor.tsx:329-333`
- **代码**`<input value={editor.title} onChange={(e) => editor.setTitle(e.target.value)} />`
- **影响**:每次按键直接触发 store 更新 → 触发 LessonPlanEditor 全量重渲染(因第 62 行无 selector
- **修复**:使用 `useDeferredValue` 或本地 state + debounce
##### 7.4.3 AttendanceSheet 多重 useState
- **位置**`src/modules/attendance/components/attendance-sheet.tsx:115-127`
- **问题**11 个 useStateclassId / date / period / statuses / reasons / searchQuery / focusedStudentIndex / isSubmitting / showSwitchConfirm / pendingClassIndex。`statuses` / `reasons` 用 `Record<string, ...>` immutable 更新,每次按键都创建新对象 → 全表重渲染
---
## 第四部分Server Action / 数据层审计
### 4.1 N+1 查询审计
#### [P0] 反例:循环单条 INSERT无事务无批量化
- **位置**`src/modules/course-plans/data-access.ts:347-406` `copyCoursePlanToClasses`
- **问题**:外层 `for (classId of targetClassIds)` 内 `await db.insert(coursePlans)`,嵌套 `for (item of sourceItems)` 内再 `await db.insert(coursePlanItems)`。10 班 × 20 周 = 200 次顺序 INSERT无事务保护无大小限制
- **影响**:复制 10 个班需 200+ 次往返;中途失败导致部分复制遗留脏数据;调用方传入 100+ classIds 时响应时间分钟级
- **修复**:改为 `db.transaction(async tx => { await tx.insert(coursePlans).values([...]) ; await tx.insert(coursePlanItems).values([...]) })`,并限制 `targetClassIds.length <= 50`
#### [P1] 反例:事务内循环 UPDATE
- **位置 1**`src/modules/homework/data-access-write.ts:326-338` `autoGradeSubmission` 事务内
- **位置 2**`src/modules/homework/data-access-write.ts:360-375` `gradeHomeworkAnswers` 事务内
- **问题**`for (const ans of gradedAnswers) { await tx.update(homeworkAnswers) }`,每道题一次 UPDATE
- **影响**30 题作业需 30 次 UPDATE事务持锁时间线性增长并发批改时锁竞争加剧
- **修复**:改用 MySQL `INSERT ... ON DUPLICATE KEY UPDATE` 或构造 `CASE WHEN id IN (...) THEN score ... END` 单条 UPDATE
#### [P2] 反例:批量删除 fallback 路径循环
- **位置**`src/modules/files/data-access.ts:174-182` `deleteFileAttachments` fallback
- **问题**:主路径已 `inArray` 批量,但 fallback 仍循环单条 delete
#### 良好实践
- ✅ `src/modules/dashboard/data-access.ts:15-30` `getAdminDashboardData` 用 `Promise.all` 并行 6 个 stats
- ✅ `src/modules/homework/stats-service.ts:58-82` `getTeacherGradeTrends` 用 `Promise.all` 并行
- ✅ `src/modules/grades/data-access.ts:96-107` `getGradeRecords` 用 `Promise.all` 并行 count + rows
### 4.2 索引缺失审计
#### Schema 索引现状
- 总表数: 50+ 张
- 已建索引: 110+ 个(含复合、唯一、外键)
- 整体索引覆盖良好,但存在以下缺口
#### [P1] 高频查询字段无索引
| 表 | 缺失索引 | 影响 |
|---|---|---|
| `exams` (schema.ts:517-561) | `creatorId` 无索引 | `getExams` 按 `creatorId` 过滤("我的试卷"),全表扫描 |
| `exams` | `status` 无索引 | `getExams` 按 status 过滤;`getHomeworkDashboardStats` 反复按 status 过滤 |
| `examQuestions` (schema.ts:565-572) | `questionId` 无索引 | 题目被引用检测、删除时反查 exam_questions 全表扫描 |
| `homeworkAssignmentQuestions` (schema.ts:635-653) | `questionId` 无索引 | 同上 |
| `submissionAnswers` (schema.ts:589-602) | `questionId` 无索引 | 题目级统计反查全表 |
#### [P2] 复合索引未覆盖单字段查询
| 表 | 现状 | 问题 |
|---|---|---|
| `examSubmissions` (schema.ts:574-587) | 仅有 `(examId, studentId)` 复合 | 单独按 `studentId` 查询(学生全部提交)走全表 |
| `homeworkAnswers` (schema.ts:701-723) | 仅有 `(submissionId, questionId)` 复合 | 单独按 `questionId` 反查(错题统计)走全表 |
#### 修复建议
```sql
CREATE INDEX exams_creator_idx ON exams(creator_id);
CREATE INDEX exams_status_idx ON exams(status);
CREATE INDEX exam_questions_question_idx ON exam_questions(question_id);
CREATE INDEX hw_assignment_questions_question_idx ON homework_assignment_questions(question_id);
CREATE INDEX exam_submissions_student_idx ON exam_submissions(student_id);
CREATE INDEX submission_answers_question_idx ON submission_answers(question_id);
CREATE INDEX hw_answers_question_idx ON homework_answers(question_id);
```
#### 良好实践
- ✅ `attendanceRecords` 索引覆盖优秀:`classDatePeriodIdx` 复合索引覆盖按节次点名场景
- ✅ `gradeRecords` 已建 `classSubjectIdx` 复合索引
- ✅ `messages` 已建 `receiverReadIdx`、`receiverStarredIdx` 复合索引
- ✅ `auditLogs` 已建 `createdAtIdx` 支持时间范围查询
### 4.3 并行数据获取审计
#### [P2] 串行 await 可优化为 Promise.all
| 文件:行号 | 反例 | 优化 |
|---|---|---|
| `src/modules/users/data-access.ts:27-37` `getUserProfile` | 先 `db.query.users.findFirst`,再 `db.select roles` —— 两次串行 await | 改 `Promise.all([findUser, findRoles])` |
| `src/modules/homework/stats-service.ts:261-302` `getStudentDashboardGrades` | 9 次串行 await | 部分可并行(如 `assignments` 与 `getAssignmentMaxScoreById` 无依赖关系) |
| `src/modules/dashboard/page.tsx:11-15` 根 dashboard 页 | `await auth()` → `await resolvePermissions(roles)` 串行 | JWT 已含 permissionsBitmap可直接 `decodePermissionsBitmap(session.user.permissionsBitmap)` 跳过 `resolvePermissions` DB 查询 |
| `src/modules/rbac/lib/data-scope-resolver.ts:71-79` parent scope | `getChildren` → `getGradeIdsForStudentIds` 串行 | 可在 `getChildren` 一并返回 studentIds → gradeIds 联表查询,消除二次往返 |
#### 良好实践
- ✅ `src/modules/dashboard/actions.ts:58-65` `getTeacherDashboardAction` 6 路并行
- ✅ `src/modules/dashboard/actions.ts:108-113` `getStudentDashboardAction` 4 路并行
- ✅ `src/modules/dashboard/data-access.ts:23-30` admin dashboard 6 路并行
- ✅ `src/modules/exams/data-access.ts:235-238` `resolveSubjectGradeNames` 2 路并行
### 4.4 缓存策略审计
#### [P0] 跨请求数据缓存完全缺失
| 现状 | 证据 |
|---|---|
| 全项目 0 处 `unstable_cache` 使用 | `Grep unstable_cache` 返回 0 匹配 |
| 全项目 0 处 `"use cache"` 指令 | Next.js 16 的 `"use cache"` 未启用 |
| 全项目 0 处 `cache: "no-store" / "force-cache"` 显式配置 | `Grep cache:\s*["']` 返回 0 匹配 |
| 全项目 0 处 `next: { revalidate }` 配置 | 同上 |
| Data-access 层仅使用 `cache()` from React请求内去重 | 例如 `src/modules/dashboard/data-access.ts:15` `cache(async ...)` |
#### 影响
每个 Server Action 调用都重新查 DB包括低频变更的高频读取数据
- `getSubjectOptions` / `getGradeOptions`(学期内几乎不变)
- `getClassesDashboardStats`(班级总数)
- `getSystemSettings`(系统配置)
- `getTextbooks`(教材列表)
- `getRoles`(角色列表)
- `resolvePermissions`(每次 JWT refresh 才查 DB但根 dashboard 仍重复查)
DataScope 每次 Server Action 调用都从 DB 重查1-3 次查询)
#### 修复建议
```ts
// 1. 低变更频率数据用 unstable_cache + tag
import { unstable_cache } from "next/cache"
export const getSubjectOptions = unstable_cache(
async () => db.select().from(subjects).orderBy(asc(subjects.name)),
["subject-options"],
{ tags: ["subjects"], revalidateTag: 3600 }
)
// 2. Dashboard stats 用 5 分钟短缓存
export const getClassesDashboardStats = unstable_cache(
async () => { /* ... */ },
["classes-dashboard-stats"],
{ tags: ["dashboard", "classes"], revalidateTag: 300 }
)
// 3. DataScope 用 React cache() 请求级去重 + 短时 unstable_cache
// 注意DataScope 变更(教师新接班级)需触发 tag 失效
```
#### 良好实践
- ✅ `revalidatePath` 在 50+ actions 文件中正确使用,路径精确(如 `/teacher/exams/all`、`/student/error-book`
- ✅ `src/auth.ts:55-86` JWT callback 在 token refresh 时刷新 roles/permissions
### 4.5 数据传输体积审计
#### [P1] 单次查询拉取过大
| 文件:行号 | 问题 | 影响 |
|---|---|---|
| `src/modules/search/data-access.ts:64-79` `searchQuestions` | `select({ content: questions.content, ... })` 把整个 JSON 题干内容(可能含图片 base64 / SlateJS 节点树)拉出,仅用于 `truncate(text, 200)` 摘要 | 单题 content 可达 10-50 KB搜索 10 条返回 500 KB+ |
| `src/modules/homework/stats-service.ts:162-172` `getHomeworkAssignmentAnalytics` | `db.query.homeworkSubmissions.findMany({ with: { answers: true, student: true } })` 一次拉取所有已批改提交 + 答案 + 学生信息,`limit: 5000`line 345 | 50 人班级 × 30 题 = 1500 行 answer + 50 行 submission + 50 行 user单次返回 MB 级 |
| `src/modules/users/data-access.ts:348-351` `getAllUserIds` | `db.select({ id: users.id }).from(users)` 无 LIMIT | 大型学校 5000+ 用户时返回 5000 行 |
| `src/modules/grades/data-access.ts:105` `getGradeRecords` | `.limit(params.limit ?? 1000)` 默认 1000 行 | 默认值偏高,前端通常只展示 20 行 |
#### [P2] 中等过度传输
- `src/modules/homework/stats-service.ts:216` `wrongAnswers.slice(0, 500)` 每题返回最多 500 个错答样本
- `src/modules/textbooks/data-access.ts:43-91` `getTextbooks` 无显式 LIMIT
#### 良好实践
- ✅ `users.password` 字段在所有 select 中均未暴露
- ✅ `getExams` 用 Drizzle `with: { subject, gradeEntity }` 关联查询,避免 N+1
- ✅ `getGradeRecords` 批量 fetch 名称而非逐行 JOIN模式正确
- ✅ `getAdminUsers` 显式 `pageSize = Math.min(100, ...)` 限制
### 4.6 权限校验性能审计
#### [P0] 每次 Server Action 调用必查 DB
调用链: `actions.ts → requirePermission() → getAuthContext() → resolveDataScope() → DATA_SCOPE_RULES[].resolve(userId) → DB`
| 角色 | DB 查询次数 | 证据 |
|---|---|---|
| admin | 0直接返回 `{type:"all"}` | `data-scope-resolver.ts:30` |
| grade_head / teaching_head | 1 次(`getGradesForStaff` | `data-scope-resolver.ts:36` |
| teacher | 2 次并行classes + subjects | `data-scope-resolver.ts:48` 调 `getTeacherScopeData` 内部 Promise.all |
| student | 1+ 次(`getStudentScopeData` | `data-scope-resolver.ts:60` |
| parent | 2 次(**串行**`getChildren` → `getGradeIdsForStudentIds` | `data-scope-resolver.ts:72-74` |
**关键证据**`src/shared/lib/auth-guard.ts:44-47`:
```ts
// Resolve data scope from DB (not cached in JWT since it can change)
const dataScope = await resolveDataScope(userId, roleNames)
```
注释明确说明"不缓存因可变",导致每次 Server Action 至少 1-2 次 DB 查询仅用于权限。
#### [P2] getSessionTeacherId 未用 React cache() 包装
- **位置**`src/modules/classes/data-access.ts:17-31` `getSessionTeacherId`
- **问题**:每次 `await auth()` + DB 查询,无 `cache()` 包装。同一请求内多次调用(如 dashboard action + 子函数)会重复查询
- **对比**`getUserProfile``users/data-access.ts:26`)、`getAdminDashboardData``dashboard/data-access.ts:15`)、`getStudentDashboardGrades``stats-service.ts:257`)均用 `cache()` 包装,请求内去重正确
#### 修复建议
```ts
// 1. getAuthContext 用 React cache() 请求级去重
export const getAuthContext = cache(async (): Promise<AuthContext> => {
const session = await getSession()
// ... existing logic
})
// 2. DataScope 短时 unstable_cache30s变更时通过 tag 失效
const resolveDataScope = unstable_cache(
async (userId, roleNames) => { /* ... */ },
[`data-scope-${userId}`],
{ tags: [`user-scope-${userId}`], revalidateTag: 30 }
)
// 3. 教师新接班级 / 学生转班时调用 revalidateTag(`user-scope-${userId}`)
// 4. getSessionTeacherId 加 cache() 包装
export const getSessionTeacherId = cache(async () => { /* ... */ })
```
#### 良好实践
- ✅ `src/auth.ts:47-83` JWT callback 在 token 创建 + refresh 时统一查询 roles + permissions
- ✅ `src/auth.ts:47-50` permissions 用 bitmap 压缩14 字符 vs 1.1 KB 数组),节省 cookie 体积
### 4.7 批量操作审计
#### [P0] 无事务、无批量、无大小限制
见 4.1 `copyCoursePlanToClasses`。
#### 良好实践
- ✅ 全项目 41+ 处 `db.transaction` 包裹写入操作
- ✅ `src/modules/grades/data-access.ts:179-201` `batchCreateGradeRecords` 正确用事务 + 批量 insert
- ✅ `src/modules/users/data-access.ts:130` 用户导入用事务
- ✅ `src/modules/elective/data-access-operations.ts` 选课操作多处用事务
### 4.8 慢查询风险审计
#### [P0] 全表扫描 / 无 LIMIT 全量返回
| 文件:行号 | 问题 |
|---|---|
| `src/modules/search/data-access.ts:74-75` `searchQuestions` | `CAST(${questions.content} AS CHAR) LIKE ${kw}` —— JSON 字段 CAST 后 LIKE无 FULLTEXT 索引,必然全表扫描 |
| `src/modules/lesson-preparation/data-access-knowledge.ts:98, 126` | `like(lessonPlans.content, \`%${...}%\`)` —— JSON content 列 LIKE |
| `src/modules/users/data-access.ts:348-351` `getAllUserIds` | `db.select({ id }).from(users)` 无 LIMIT无 WHERE |
| `src/modules/announcements/data-access.ts:560-561` | school 公告 fan-out 调 `getAllUserIds()`5000+ 用户全量加载到内存 |
| `src/modules/files/data-access.ts:128-139` `getAllFileAttachments` | 全量返回文件表,无分页 |
#### [P1] LIKE 前缀通配 `%xxx%`(无法走索引)
| 文件:行号 | 字段 |
|---|---|
| `src/modules/textbooks/data-access.ts:50-53` | title / subject / grade / publisher 4 列 `%${q}%` |
| `src/modules/exams/data-access.ts:70` | title / description `%${q}%` |
| `src/modules/standards/data-access.ts:147` | title `%${keyword}%` |
| `src/modules/lesson-preparation/data-access.ts:238` | title `%${q}%` |
| `src/modules/audit/data-access.ts:46` | action `%${params.action}%` |
| `src/modules/users/data-access.ts:384` | `ilike(users.name, search)` / `ilike(users.email, search)` |
| `src/modules/rbac/data-access-assignments.ts:103` | `ilike(users.name, term)` / `ilike(users.email, term)` |
| `src/modules/messaging/data-access.ts:140` | `like(messages.subject, kw)` / `like(messages.content, kw)` |
✅ 例外:`src/modules/files/data-access.ts:204` `like(fileAttachments.mimeType, \`${mimeType}%\`)` 是前缀匹配,可走索引
#### [P2] 高 LIMIT 默认值
| 文件:行号 | LIMIT |
|---|---|
| `src/modules/grades/data-access.ts:105` `getGradeRecords` | 默认 1000 |
| `src/modules/homework/stats-service.ts:276` `getStudentDashboardGrades` | `limit: 200` |
| `src/modules/homework/stats-service.ts:345` `classGradedSubmissions` | `limit: 5000` |
| `src/modules/homework/stats-service.ts:216` | `wrongAnswers.slice(0, 500)` |
| `src/modules/adaptive-practice/data-access-strategy.ts:293` | `.limit(1000)` |
#### 修复建议
- **搜索 API 改用 MySQL FULLTEXT 索引**
```sql
ALTER TABLE questions ADD FULLTEXT INDEX ft_content (content);
ALTER TABLE announcements ADD FULLTEXT INDEX ft_title_content (title, content);
-- 查询改用 MATCH ... AGAINST
```
- `getAllUserIds` 改为游标分页 + fan-out 改为批量插入 `message_notifications`(每次 500 条)
- `LIKE %xxx%` 改为前缀匹配 `LIKE xxx%` 或 ES/OpenSearch 外部搜索引擎
- 高 LIMIT 改为基于业务实际值:`classGradedSubmissions` 50 人班级够用 1000
---
## 第五部分:综合性能预算基线表
### 5.1 Bundle 体积预算gzip 后)
| 维度 | 预算 | 强制等级 |
|------|------|---------|
| 落地页 / 登录 First Load JS | ≤ 180 KB | 强制 |
| 学生端 Dashboard First Load JS | ≤ 250 KB | 强制 |
| 教师端 Dashboard First Load JS | ≤ 280 KB | 强制 |
| 列表/详情页 First Load JS | ≤ 300 KB | 强制 |
| 教材阅读页 First Load JS含 ReactFlow + tiptap + force-graph | ≤ 600 KB | 强制 |
| 试卷富文本编辑页 First Load JS | ≤ 500 KB | 强制 |
| 备课编辑页 First Load JS | ≤ 550 KB | 强制 |
| 单路由增量 chunk动态加载组件 | ≤ 200 KB | 推荐 |
| 共享框架 chunkreact/react-dom/next | ≤ 130 KB | — |
| 单文件体积上限 | ≤ 1000 行 | 强制(已有 lint 规则) |
### 5.2 Web Vitals 预算P95
| 指标 | 预算 | 强制等级 |
|------|------|---------|
| LCP | ≤ 2.5s(备课编辑器允许 ≤ 3.0s | 强制 |
| INP | ≤ 200ms仪表盘图表允许 ≤ 300ms | 强制 |
| CLS | ≤ 0.05 | 强制 |
| TTFB | ≤ 800msdashboard 路由) | 强制 |
| FCP | ≤ 1.8s | 强制 |
### 5.3 数据层性能预算
| 维度 | 预算 | 强制等级 |
|------|------|---------|
| 单次 Server Action 响应时间P50 | ≤ 200 ms | 强制 |
| 单次 Server Action 响应时间P99 | ≤ 800 ms | 强制 |
| 单次 DB 查询执行时间 | ≤ 50 ms | 强制 |
| Server Action 内 DB 查询次数 | ≤ 5 次 | 强制 |
| 单次查询返回行数 | ≤ 200 行 | 强制 |
| 单次查询返回数据体积 | ≤ 100 KB | 强制 |
| N+1 查询 | **严格禁止** | 强制CI 检查) |
| 循环内 `await db.insert/update/delete` | **严格禁止**(除非有 fallback 注释) | 强制 |
| 权限校验 DB 查询次数 | ≤ 1 次/请求(用 React `cache()` 去重) | 强制 |
| DataScope 解析 DB 查询次数 | ≤ 1 次/请求30s unstable_cache | 强制 |
| 跨请求缓存命中率dashboard/统计) | ≥ 80% | 推荐 |
| `revalidatePath` 路径精确度 | ≤ 2 条/写操作 | 强制 |
| 批量操作事务包裹率 | 100% | 强制 |
| 批量操作大小限制 | ≤ 50 行/事务 | 强制 |
| LIKE 前缀通配 `%xxx%` 出现次数 | 0除非有 FULLTEXT 索引) | 强制 |
| 全表扫描查询(无 LIMIT + 无 WHERE | 0 | 强制 |
| 默认分页 size | ≤ 50 | 推荐 |
| 单条返回 JSON 字段体积 | ≤ 50 KB | 强制 |
| Server → Client 传输 `password`/`passwordHash`/`token` 字段 | 0 | 强制 |
### 5.4 关键 KPI建议接入监控
- Server Action P50/P99 响应时间(按 action 名分维度)
- DB 查询次数 / Server Action 调用
- 慢查询(>50ms次数与 SQL 摘要
- DataScope 解析耗时
- `unstable_cache` 命中率
- N+1 自动检测(建议接入 `drizzle-orm/dev` 的 query log + CI 规则)
- Web Vitals 上报LCP/CLS/INP/TTFB/FCP
- Bundle 体积变化(每次 PR 与基线对比)
---
## 第六部分P0/P1 修复路线图
### 6.1 P0 修复路线图(按 ROI 排序)
| # | 项目 | 涉及章节 | 预期收益 | 单点改动 | 风险 |
|---|------|---------|---------|---------|------|
| 1 | 启用 React Compiler | §3.6 | 一次性解决 §3.1 大部分内联对象问题 | ✅ | 低( Compiler 有 fallback |
| 2 | `next.config.ts` 完善optimizePackageImports + serverExternalPackages | §1.5 | 全路由 -30~80 KB + 防止 2-3 MB 误打包 | ✅ | 低 |
| 3 | tiptap / ReactFlow 动态导入4 处文件) | §1.3 | 编辑器路由 chunk 减少 ~300 KB | ✅ | 低 |
| 4 | `exams/editor/index.ts` barrel 拆分 | §1.6 | 修复已存在的构建错误 + 防止 server 误拉 tiptap | ✅ | 中(需测试 server 引用路径) |
| 5 | 接入 Web Vitals 上报 | §2.6 | 生产环境性能可见性 | ✅ | 低 |
| 6 | `SidebarProvider` value memoize + resize 防抖 | §3.2 | 全站 dashboard 重渲染减少 50%+ | ✅ | 低 |
| 7 | `lesson-plan-editor.tsx:62` 拆分 selector | §3.4, §3.7 | 备课编辑器按键响应速度提升 5-10x | ✅ | 中(需测试编辑器交互) |
| 8 | 题库表启用虚拟化 | §3.3, §3.7 | 1000+ 题时首屏可交互时间从秒级降到 100ms | ❌(需引入 @tanstack/react-virtual | 中 |
| 9 | `copyCoursePlanToClasses` 改为事务 + 批量 INSERT | §4.1, §4.7 | 复制 10 班从分钟级降到秒级 | ✅ | 低 |
| 10 | `unstable_cache` 引入DataScope + 低变更数据) | §4.4, §4.6 | 单次 Server Action DB 查询数减半 | ❌(需引入 tag 失效机制) | 中(缓存一致性) |
| 11 | `searchQuestions` 改用 FULLTEXT 索引 | §4.5, §4.8 | 搜索接口从全表扫描降到索引扫描 | ❌(需 DB 迁移) | 中(需测试搜索召回率) |
| 12 | `getAllUserIds` 加 LIMIT + 公告 fan-out 改批量 | §4.5, §4.8 | 防止 5000+ 用户场景内存爆炸 | ✅ | 低 |
### 6.2 P1 修复路线图
| # | 项目 | 涉及章节 | 预期收益 |
|---|------|---------|---------|
| 1 | `serverExternalPackages` 追加 `exceljs` | §1.5 | 防止 250-300 KB 误打包 |
| 2 | `chart.tsx` 改 barrel 为具名导入 | §1.4 | 全图表路由 -20~40 KB |
| 3 | 移除 `status-badge.tsx` 等过度标记的 `"use client"` | §1.2 | 提升 SSR 覆盖率 |
| 4 | 4 个角色根路由新增 `loading.tsx` | §2.1 | LCP/CLS 改善 |
| 5 | 96 处 `force-dynamic` 评估改 `revalidate` | §2.1 | TTFB/LCP 静态缓存命中加速 |
| 6 | Lighthouse CI 接入 | §2.6 | CI 流水线 Web Vitals 回归门槛 |
| 7 | 根 `layout.tsx` 串行 await 改 `Promise.all` | §2.1 | TTFB/FCP 改善 |
| 8 | `autoGradeSubmission` / `gradeHomeworkAnswers` 改批量 UPDATE | §4.1 | 30 题作业 UPDATE 次数从 30 降到 1 |
| 9 | exams 表新增 5 个索引 | §4.2 | "我的试卷"等高频查询全表 → 索引扫描 |
| 10 | recharts props 提升为模块级常量 | §3.1 | 仪表盘图表重渲染减少 |
| 11 | `useOptimistic` 用于消息星标 / 出勤 / 成绩 | §3.6 | 替代手写 override stateUX 提升 |
| 12 | 考试表启用虚拟化 | §3.3 | 数据量增长时避免卡顿 |
| 13 | 高频子组件 `React.memo`MessageCard、TreeNodeRow、InlineNode、AttendanceRow | §3.1 | 阻断父级渲染扩散 |
### 6.3 P2 跟进项
- `useModal` 修复(如计划采用)
- `AuditLogDetailDialog` 懒加载
- `use()` Hook 替代部分 Suspense 嵌套
- 备课编辑器 title 输入 `useDeferredValue`
- `getSessionTeacherId` 加 `cache()` 包装
- parent DataScope 改为单次 JOIN
- 默认 LIMIT 调低
- `metadata` 补充 `metadataBase` / `openGraph` / `robots`
- Playwright 性能断言
- `AiAssistantWidget` 懒加载
- scan-image-viewer `<img>` 加 aspect-ratio
- 备课编辑器 textbook → chapters 改 `Promise.all`
### 6.4 建议执行顺序
```
Phase 1: 配置基线(无业务代码改动)
├── next.config.ts 完善
├── React Compiler 启用
├── Web Vitals 上报接入
└── Lighthouse CI 接入
Phase 2: Bundle 瘦身(影响编辑器路由)
├── tiptap / ReactFlow 动态导入
├── exams/editor barrel 拆分
└── 重新 next build 获取实际体积数据
Phase 3: 数据层重构(影响所有 Server Action
├── unstable_cache 引入
├── DataScope 缓存
├── copyCoursePlanToClasses 改造
└── exams 索引补齐
Phase 4: 组件渲染优化
├── SidebarProvider memoize
├── lesson-plan-editor selector 拆分
├── 题库表虚拟化
└── recharts props 常量化
Phase 5: 验收与基线校准
├── 实际 Web Vitals 数据采集
├── 实际 Bundle 体积对照基线表
└── 预算阈值写入 CI 阻断规则
```
---
## 附录 A关键文件清单
### 配置文件
- `e:\Desktop\CICD\next.config.ts`
- `e:\Desktop\CICD\package.json`
- `e:\Desktop\CICD\playwright.config.ts`
- `e:\Desktop\CICD\vitest.config.ts`
### 入口与 Provider
- `e:\Desktop\CICD\src\app\layout.tsx`
- `e:\Desktop\CICD\src\app\providers.tsx`
- `e:\Desktop\CICD\src\app\(dashboard)\layout.tsx`
- `e:\Desktop\CICD\src\app\manifest.ts`
- `e:\Desktop\CICD\src\proxy.ts`
- `e:\Desktop\CICD\src\instrumentation.ts`
- `e:\Desktop\CICD\src\auth.ts`
### 重型组件Bundle 风险点)
- `e:\Desktop\CICD\src\modules\textbooks\components\knowledge-graph.tsx`ReactFlow未 dynamic
- `e:\Desktop\CICD\src\modules\textbooks\components\force-graph.tsx`react-force-graph-2d已 dynamic ✅)
- `e:\Desktop\CICD\src\shared\components\ui\rich-text-editor.tsx`tiptap
- `e:\Desktop\CICD\src\modules\exams\editor\exam-rich-editor.tsx`tiptap
- `e:\Desktop\CICD\src\modules\lesson-preparation\components\blocks\rich-text-block.tsx`tiptap
### Barrel 文件(高风险)
- `e:\Desktop\CICD\src\modules\exams\editor\index.ts`(已造成构建错误)
### 重渲染热点(组件性能风险点)
- `e:\Desktop\CICD\src\modules\layout\components\sidebar-provider.tsx`
- `e:\Desktop\CICD\src\modules\lesson-preparation\components\lesson-plan-editor.tsx`
- `e:\Desktop\CICD\src\modules\lesson-preparation\components\paper-editor\paper-editor.tsx`
- `e:\Desktop\CICD\src\modules\lesson-preparation\components\structure-tree\structure-tree.tsx`
- `e:\Desktop\CICD\src\modules\dashboard\components\admin-dashboard\user-growth-chart.tsx`
- `e:\Desktop\CICD\src\modules\questions\components\question-data-table.tsx`
- `e:\Desktop\CICD\src\modules\exams\components\exam-data-table.tsx`
### 数据层风险点
- `e:\Desktop\CICD\src\modules\course-plans\data-access.ts`copyCoursePlanToClasses P0
- `e:\Desktop\CICD\src\modules\homework\data-access-write.ts`(循环 UPDATE P1
- `e:\Desktop\CICD\src\modules\search\data-access.ts`LIKE 全表扫描 P0
- `e:\Desktop\CICD\src\modules\users\data-access.ts`getAllUserIds 无 LIMIT P0
- `e:\Desktop\CICD\src\shared\lib\auth-guard.ts`DataScope 每次重查 P0
- `e:\Desktop\CICD\src\shared\db\schema.ts`exams 索引缺失 P1
- `e:\Desktop\CICD\src\modules\homework\stats-service.ts`5000 行 + with 关联 P1
### 缺失文件清单(建议新增)
- `src/app/(dashboard)/teacher/loading.tsx`
- `src/app/(dashboard)/student/loading.tsx`
- `src/app/(dashboard)/parent/loading.tsx`
- `src/app/(dashboard)/management/loading.tsx`
- `e:\Desktop\CICD\.gitea\workflows\lighthouse.yml`(建议新增 Lighthouse CI
- `e:\Desktop\CICD\src\app\instrumentation-client.ts`(建议实现 `useReportWebVitals`
- `e:\Desktop\CICD\src\app\api\web-vitals\route.ts`(建议新增指标上报端点)
---
## 附录 B审计涉及的依赖清单
### 客户端大体积依赖(按估算体积排序)
| 依赖 | 估算体积min+gzip | 用途 | 当前问题 |
|------|---------------------|------|---------|
| `@tiptap/*`5 包 + tiptap-markdown | ~180-220 KB | 富文本编辑器3 处) | 未动态导入P0 |
| `@xyflow/react` | ~120-140 KB | ReactFlow 知识图谱 | 未动态导入P0 |
| `recharts` | ~95-110 KB | 图表库13 处) | chart.tsx barrel importP1 |
| `react-force-graph-2d` | ~80-100 KB | 引力图谱 | 已动态导入 ✅ |
| `react-markdown` + remark-* + rehype-* | ~50-70 KB | Markdown 渲染 | — |
| `openai` | ~60-80 KB | AI SDK | 服务端 |
| `@tanstack/react-query` + `react-table` | ~35-45 KB | 数据层 | — |
| `@dnd-kit/*`core+sortable+utilities | ~30-40 KB | 拖拽 | — |
| `radix-ui/*`16 个包) | 单包 ~3-8 KB | UI 原语 | — |
| `lucide-react` | 单图标 ~1 KB全集 ~800 KB | 图标库100+ 处) | 缺 optimizePackageImportsP1 |
### 服务端大体积依赖(防误打包)
| 依赖 | 估算体积 | 用途 | 当前状态 |
|------|----------|------|---------|
| `tencentcloud-sdk-nodejs` | **~2-3 MB** | 腾讯云短信 | 已动态 import ✅,但未加 serverExternalPackagesP0 |
| `exceljs` | ~250-300 KB | Excel 导入导出 | 静态 import未加 serverExternalPackagesP1 |
| `mysql2` | — | MySQL 驱动 | 已加 serverExternalPackages ✅ |
### 缺失依赖(建议新增)
| 依赖 | 用途 | 必要性 |
|------|------|--------|
| `babel-plugin-react-compiler` | React Compiler 自动 memoization | P0 |
| `eslint-plugin-react-compiler` | React Compiler ESLint 规则 | P0 |
| `web-vitals` | Web Vitals 上报 | P0 |
| `@tanstack/react-virtual` | 题库/考试表虚拟化 | P0 |
| `@lh3/cli` | Lighthouse CI | P1 |
---
## 审计结论
本次性能预算审计完成,共发现 **15 个 P0 严重问题**、**13 个 P1 高优先级问题**、**22 个 P2 中等优先级问题**、**14 个 P3 低优先级问题**。
**核心建议**:按第六部分"建议执行顺序"分 5 个 Phase 推进,**Phase 1 配置基线**4 项单点改动,低风险高收益)应作为首期落地,**Phase 2 Bundle 瘦身**和 **Phase 3 数据层重构**为中期重点,**Phase 4 组件渲染优化**为长期持续工作,**Phase 5 验收与基线校准**闭环反馈到预算阈值。
**关键提醒**:当前 `next build` 因 Turbopack 编译错误未成功出包,本报告所有量化指标为静态估算。修复 Phase 1 + Phase 2 后必须重新构建,获取实际 Bundle 体积与运行时 Web Vitals 数据,再对照第五部分基线表进行第二轮校准。
**架构图同步建议**:本审计为只读研究,未修改源码,**不需要同步更新 `docs/architecture/004_architecture_impact_map.md` 与 `005_architecture_data.json`**。若后续落实任何修复(如新增 loading.tsx、修改 next.config.ts、新增 web-vitals 上报、引入 unstable_cache需按项目规则同步架构图。