diff --git a/docs/architecture/004_architecture_impact_map.md b/docs/architecture/004_architecture_impact_map.md index 568c06f..96db0ca 100644 --- a/docs/architecture/004_architecture_impact_map.md +++ b/docs/architecture/004_architecture_impact_map.md @@ -286,6 +286,105 @@ V5 状态管理统一的后续阶段,针对两类高频性能问题进行细 --- +## 1.1.6 缓存策略落地专项重构(2026-07-05 新增) + +> **目标**:全栈缓存策略一致性重构 — 服务端 `cacheFn` 双层包装 + 集中式 `INVALIDATION_MAP` 失效编排 + 客户端 `queryKeys` 工厂 + `useActionQuery`/`useActionMutation` 向后兼容双模式 Hook。 + +### 架构总览 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Client(TanStack Query + useActionQuery/useActionMutation) │ +│ queryKeys.{module}.{resource}(...args) → queryKey │ +│ useActionMutation({ mutationFn, actionId, params }) │ +│ → 成功后按 CLIENT_INVALIDATION_MAP[actionId].queryKeys 自动失效 │ +└──────────────────────────────┬──────────────────────────────────────┘ + │ mutationFn = Server Action + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Server Actions(编排层) │ +│ mutation 成功分支末尾: │ +│ await invalidateFor("module.action", { id }) │ +│ → 1) CacheStore.invalidateTags 2) revalidateTag × N 3) revalidatePath × N │ +└──────────────────────────────┬──────────────────────────────────────┘ + │ 调用 data-access + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Data-access(数据访问层) │ +│ export const fnRaw = async (...) => { ... } │ +│ export const fn = cacheFn(fnRaw, { tags, ttl, keyParts }) │ +│ → 外层 react.cache(请求级 memoization) │ +│ → 内层 cacheStore.getOrSet(跨请求/跨实例,按 tag 失效) │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ CacheStore(CACHE_DRIVER 切换) │ +│ memory(默认,单实例,Map + LRU + tag 反查索引) │ +│ redis(@upstash/redis,多实例共享,故障降级) │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### 核心基础设施(9 个文件) + +| 文件 | 职责 | +|------|------| +| `shared/lib/cache/types.ts` | `CacheFnOptions` / `CacheStore` / `InvalidationRule` 接口 | +| `shared/lib/cache/memory-store.ts` | `MemoryCacheStore`(Map + LRU maxEntries=500 + tag 反查索引) | +| `shared/lib/cache/redis-store.ts` | `RedisCacheStore`(key=`next-edu:cache:{key}`,故障降级) | +| `shared/lib/cache/store-factory.ts` | `getCacheStore()` 单例工厂(按 `env.CACHE_DRIVER` 切换) | +| `shared/lib/cache/cache-fn.ts` | `cacheFn` 双层包装器(外层 react.cache + 内层 cacheStore.getOrSet) | +| `shared/lib/cache/invalidation-map.ts` | `INVALIDATION_MAP` 集中式失效映射(203 个 actionId)+ `fillTemplate` | +| `shared/lib/cache/client-invalidation-map.ts` | `CLIENT_INVALIDATION_MAP` 客户端可见子集(仅 queryKeys) | +| `shared/lib/cache/invalidate.ts` | `invalidateFor` 三步编排函数 | +| `shared/lib/cache/index.ts` | barrel re-export | +| `shared/lib/redis-client.ts` | 共享 Redis 单例(rate-limit + cache 共用) | +| `shared/lib/query-keys.ts` | 客户端 queryKey 工厂(`queryKeys.classes.*`) | + +### 迁移覆盖范围(28 个模块全部完成) + +**data-access 层迁移至 `cacheFn`**(83 个文件,250+ 个查询函数): + +| 组别 | 模块 | 文件数 | 备注 | +|------|------|--------|------| +| 标杆 | classes | 5 | `data-access-{teacher,admin,students,stats,schedule}.ts` | +| A | users / school / rbac / textbooks / questions / course-plans / files / dashboard / parent / proctoring | 11 | 77 个函数 | +| B | exams / grades / homework / diagnostic / error-book / adaptive-practice | 8 | 含跨模块接口 | +| C | lesson-preparation / elective / announcements / messaging / notifications | 6 | 24 个函数 | + +**actions 层迁移至 `invalidateFor`**(43 个文件,247 处 `revalidatePath` 替换): + +| 组别 | 模块 | 文件数 | 替换数 | +|------|------|--------|--------| +| 标杆 | classes | 5 | 47 | +| A | users / school / textbooks / questions / course-plans / standards / scheduling / audit / onboarding / invitation-codes / proctoring / i18n | 13 | 70 | +| B | exams / grades / homework / attendance / leave-requests / diagnostic / error-book / adaptive-practice | 9 | 77 | +| C | lesson-preparation / elective / announcements / messaging / notifications / settings / rbac | 8 | 100 | + +**INVALIDATION_MAP**:扩展至 **203 个 actionId**,覆盖全部 28 个模块。每个 actionId 声明 `{ tags, queryKeys, paths }` 三段副作用。 + +### 强制规则(已通过 ESLint 落地) + +| 规则 | 落地方式 | +|------|----------| +| Server Actions 中禁止直接调用 `revalidatePath` / `revalidateTag` | `no-restricted-syntax` ESLint 规则,仅 `shared/lib/cache/invalidate.ts` 豁免 | +| data-access 查询函数必须用 `cacheFn` 包装 | 双导出 raw 模式(`fnRaw` + `cacheFn(fnRaw, opts)`),单测可 mock | +| 未知 actionId 必须抛错 | `invalidateFor` 内部强校验,提示更新 INVALIDATION_MAP | +| Tag 命名规范 | `{module}` / `{module}:{resource}` / `{module}:{resource}:{id}` | + +### 向后兼容 Hook 双模式 + +- **`useActionQuery`**:传 `queryKey` 走 `useQuery`(跨页共享缓存 + 自动 invalidate);不传走旧 `useEffect + useState`(向后兼容) +- **`useActionMutation`**:传 `mutationFn + actionId` 自动 invalidate 相关 queryKeys;旧 `mutate(action)` 模式仍可用 + +### 验证结果 + +- `npx tsc --noEmit`:零错误 +- `npm run lint`:零 `no-restricted-syntax` 错误(仅 3 个 pre-existing `react-hooks/refs` 错误位于未修改文件) +- `npx vitest run src/shared/lib/cache/`:25/25 测试通过(5 个测试文件:types/memory-store/redis-store/cache-fn/invalidate/invalidation-map) + +--- + ## 1.2 模块依赖关系图 下图展示模块间的实际依赖关系,**标注依赖类型与合规性**。 diff --git a/docs/architecture/005_architecture_data.json b/docs/architecture/005_architecture_data.json index c7ce9da..1f4ce72 100644 --- a/docs/architecture/005_architecture_data.json +++ b/docs/architecture/005_architecture_data.json @@ -5,7 +5,7 @@ "generatedAt": "2026-06-17", "formatVersion": "1.1", "rule": "每次文件修改后须同步更新本文件", - "lastUpdate": "性能预算重构专项 Phase 2-4 完成(2026-07-05):Phase 2 Bundle 预算优化:(2.1) TipTap 三实例(exam-rich-editor/question-rich-editor/paper-editor)+ AiAssistantWidget 改 next/dynamic ssr:false + xxx-inner.tsx 拆分模式;(2.2) KnowledgeGraph 改 ReactFlowProvider 同步壳 + lazy 内部实现;(2.3) exams/editor 拆 types.ts barrel 减少服务端消费方拉入 @tiptap/* 运行时;(2.4) status-badge 移除多余 'use client';(2.5) teacher/student/parent 角色路由补齐 loading.tsx。Phase 3 数据层优化:(3.1) getSession/getAuthContext/resolveDataScope/getSessionTeacherId 全部 React cache() 包裹实现请求级去重;(3.2) copyCoursePlanToClasses 改单事务 + 2 次 batch INSERT;(3.3) gradeHomeworkAnswers 改 UPDATE CASE WHEN 单次批量更新;(3.4) searchQuestions 改 FULLTEXT INDEX + MATCH AGAINST BOOLEAN MODE + toBooleanModeQuery 工具;(3.5) getAllUserIds 加默认 LIMIT 1000 + 分页参数;(3.6) announcements/notifications fan-out 改 createNotifications 批量 INSERT + sendBatchNotifications 并行收集;(3.7) 高 LIMIT 默认值调低(attendance/adaptive-practice/questions data-access 1000-5000→100)。Phase 4 组件渲染优化:(4.1) AssignmentCard/EmptyState/StatusBadge React.memo;(4.2) lesson-plan-editor/text-study-block/exercise-block/paper-context-menu 拆分 Zustand 细粒度 selector(避免整体订阅);(4.3) message-detail/message-list/grade-record-list/attendance-sheet 改 useOptimistic + useTransition 乐观更新;(4.4) question-data-table/exam-data-table 接入 @tanstack/react-virtual 列表虚拟化;(4.5) sidebar-provider resize 改 debounce + useMemo;(4.6) 14 个 recharts 组件 props 提取到模块级常量避免 inline 重建;(4.7) scan-image-viewer aspect-ratio;(4.8) AiAssistantWidget inner 拆分 + 动态 import;(4.9) web-vitals-reporter.tsx 修复 Metric 类型推导(从 useReportWebVitals 签名推导)+ PerformanceEntry 类型注解 + sendBeacon/fetch keepalive fallback。架构决策:(1) 动态 import 标准模式 xxx-inner.tsx + lazy wrapper;(2) 请求级去重 > 跨请求 cache(React cache() not unstable_cache);(3) 批量 SQL > 循环 SQL(INSERT batch + UPDATE CASE WHEN);(4) React 19 useOptimistic 替代手动 isPending;(5) 细粒度 Zustand selector > useShallow 多字段 selector。验证:npx tsc --noEmit 零新增错误;npm run lint 零新增错误(pre-existing 3 errors + 13 warnings 均位于未修改文件)。", + "lastUpdate": "缓存策略落地专项重构完成(2026-07-05):全栈缓存一致性重构,覆盖全部 28 个模块。基础设施层:新增 shared/lib/cache/ 9 个文件(types/memory-store/redis-store/store-factory/cache-fn/invalidation-map/client-invalidation-map/invalidate/index)+ 共享 redis-client.ts + 客户端 query-keys.ts 工厂。核心机制:(1) cacheFn 双层包装器 — 外层 react.cache 请求级 memoization,内层 cacheStore.getOrSet 跨请求/跨实例缓存(按 tag 失效,TTL 可选);(2) INVALIDATION_MAP 集中式失效映射表 — 扩展至 203 个 actionId,每个声明 { tags, queryKeys, paths } 三段副作用;(3) invalidateFor 三步编排 — CacheStore.invalidateTags → revalidateTag × N → revalidatePath × N(Next.js 16 revalidateTag 第二参数必填 \"default\");(4) CLIENT_INVALIDATION_MAP 客户端可见子集(仅 queryKeys 字段,避免拉入 server-only 依赖);(5) CACHE_DRIVER 环境变量切换 memory/redis driver,复用 rate-limit 模式。迁移范围:data-access 83 个文件 250+ 查询函数迁移至 cacheFn(双导出 raw + 包装版本,单测可 mock);actions 43 个文件 247 处 revalidatePath 替换为 invalidateFor;INVALIDATION_MAP 覆盖全部 28 个模块。Hook 双模式:useActionQuery 传 queryKey 走 useQuery 跨页共享缓存,不传走旧 useEffect+useState;useActionMutation 传 mutationFn+actionId 自动 invalidate,旧 mutate(action) 仍可用。ESLint 强制规则:no-restricted-syntax 禁止 Server Actions 中直接调用 revalidatePath/revalidateTag,仅 shared/lib/cache/invalidate.ts 豁免;data-access 查询函数双导出 raw 模式便于单测 mock。验证:npx tsc --noEmit 零错误;npm run lint 零 no-restricted-syntax 错误(仅 3 个 pre-existing react-hooks/refs 错误位于未修改文件);npx vitest run src/shared/lib/cache/ 25/25 测试通过。详细文档见 004 第 1.1.6 节。", "lastUpdated": "2026-07-05" }, "architectureOverview": { diff --git a/docs/architecture/audit/performance-budget-audit-report.md b/docs/architecture/audit/performance-budget-audit-report.md new file mode 100644 index 0000000..6721a53 --- /dev/null +++ b/docs/architecture/audit/performance-budget-audit-report.md @@ -0,0 +1,1175 @@ +# 性能预算重构专项 - 完整审计报告 + +> **审计日期**: 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`) +- ✅ `