perf(phase1-4): 补漏性能预算重构专项遗漏项

Phase 1 配置基线:next.config.ts 新增 experimental.optimizePackageImports(lucide-react/recharts/@xyflow/react/@tiptap/* /@radix-ui/*/date-fns)+ serverExternalPackages 追加 tencentcloud-sdk-nodejs + exceljs;providers.tsx 注入 WebVitalsReporter 闭环 RUM 上报链路(之前组件存在但未注入导致生产环境零性能数据)。

Phase 2 Bundle 预算优化补漏:TipTap 三实例(shared/ui/rich-text-editor + exams/editor/exam-rich-editor + lesson-preparation/blocks/rich-text-block)之前仅文件拆分未真正 next/dynamic ssr:false,本次补齐 lazy wrapper + xxx-inner.tsx 拆分模式;ReactFlow knowledge-graph.tsx 改用 next/dynamic 加载 inner;chart.tsx 改 recharts 具名导入替代 import * as RechartsPrimitive barrel。

Phase 4 组件渲染优化补漏:12 个 recharts 图表组件(parent/grades/dashboard/homework 模块下)margin props 提取到模块级 CHART_MARGIN 常量避免 inline 重建;layout.tsx metadata 增强(metadataBase/openGraph/twitter/robots/authors/creator)+ getLocale/getMessages/auth 串行 await 改 Promise.all 并行。

同步架构文档 004/005 + known-issues.md:新增 Phase 1 配置基线规则章节 + Phase 2 文件清单修正(之前引用不存在的 question-rich-editor.tsx 和 paper-rich-editor.tsx,实际是 rich-text-editor.tsx 和 rich-text-block.tsx)+ Phase 4.9 metadata + Promise.all 规则补充。架构图 1.1.5 章节新增未完成项(React Compiler/force-dynamic 评估/Playwright 性能断言)。

验证:npx tsc --noEmit 零错误;npm run lint 零新增错误(3 errors + 12 warnings 均位于未修改的 pre-existing 文件)。
This commit is contained in:
SpecialX
2026-07-07 00:30:47 +08:00
parent 6104e6a685
commit 94f098b0f2
25 changed files with 335 additions and 830 deletions

View File

@@ -241,19 +241,24 @@ V5 状态管理统一的后续阶段,针对两类高频性能问题进行细
## 1.1.5 性能预算重构专项2026-07-05 新增)
## 1.1.5 性能预算重构专项2026-07-05 新增 + 2026-07-07 补漏
> 完整审计报告见 `docs/architecture/audit/performance-budget-audit-report.md`。
> 本章节记录本次重构对架构产生的关键变更,以便后续维护时定位。
> 2026-07-07 补漏Phase 1 配置基线next.config.ts optimizePackageImports + serverExternalPackages 扩展 + WebVitalsReporter 注入闭环)+ Phase 2 实际动态导入实现TipTap 3 处 + ReactFlow 1 处之前仅文件拆分未真正 dynamic import,本次补齐 next/dynamic ssr:false+ chart.tsx 具名导入 + 12 个 recharts 图表 CHART_MARGIN 提取 + layout.tsx Promise.all + metadata 增强。
### 影响范围概览
| Phase | 改造内容 | 关键文件 |
|-------|---------|---------|
| 2.1 | tiptap 动态导入(3 处) | `shared/components/ui/rich-text-editor.tsx` + `modules/exams/editor/exam-rich-editor.tsx` + `modules/lesson-preparation/components/blocks/rich-text-block.tsx` |
| 2.2 | ReactFlow 动态导入 | `modules/textbooks/components/knowledge-graph.tsx` 拆为 `knowledge-graph-inner.tsx` + lazy wrapper,ReactFlowProvider 留外层 |
| 1.1 | next.config.ts optimizePackageImports | `next.config.ts` 新增 `experimental.optimizePackageImports` 覆盖 lucide-react/recharts/@xyflow/react/@tiptap/* /@radix-ui/*/date-fns |
| 1.2 | serverExternalPackages 扩展 | `next.config.ts` 追加 `tencentcloud-sdk-nodejs` + `exceljs`(原仅 `mysql2` |
| 1.3 | Web Vitals 上报闭环 | `src/app/providers.tsx` 注入 `<WebVitalsReporter />`(之前组件存在但未注入,链路断裂) |
| 1.4 | Lighthouse CI | `.gitea/workflows/lighthouse.yml` + `lighthouserc.json`PR + 每日凌晨 3 点采样,LCP/CLS error 级) |
| 2.1 | tiptap 动态导入(3 处) | `shared/components/ui/rich-text-editor.tsx` + `rich-text-editor-inner.tsx`;`modules/exams/editor/exam-rich-editor.tsx` + `exam-rich-editor-inner.tsx`forwardRef wrapper;`modules/lesson-preparation/components/blocks/rich-text-block.tsx` + `rich-text-block-inner.tsx` |
| 2.2 | ReactFlow 动态导入 | `modules/textbooks/components/knowledge-graph.tsx` 改用 `next/dynamic` 加载 `knowledge-graph-inner.tsx`,ReactFlowProvider 留外层同步壳 |
| 2.3 | exams/editor barrel 拆分 | 新增 `modules/exams/editor/types.ts` 作为纯类型入口,供 server-only 消费方引用 |
| 2.4 | chart.tsx barrel + status-badge 移除 use client | chart.tsx 已是具名导出;`status-badge.tsx` 移除 `"use client"`(纯展示组件) |
| 2.4 | chart.tsx 具名导入 + status-badge 移除 use client | `shared/components/ui/chart.tsx` 改 `import { ResponsiveContainer, Tooltip, Legend } from "recharts"` 替代 `import * as RechartsPrimitive`;`status-badge.tsx` 移除 `"use client"`(纯展示组件) |
| 2.5 | 4 个角色根路由 loading.tsx | 新增 `teacher/loading.tsx` + `student/loading.tsx` + `parent/loading.tsx`(admin 已有) |
| 3.1 | unstable_cache 评估 | 评估后决定不引入,与项目现有 React `cache()` 模式一致 |
| 3.2 | auth 函数 cache 包装 | `shared/lib/session.ts:getSession` + `shared/lib/auth-guard.ts:resolveDataScope` + `getAuthContext` + `modules/classes/data-access.ts:getSessionTeacherId` 全部用 React `cache()` 包装,`getSessionTeacherId` 改走 `getAuthContext()` |
@@ -266,12 +271,18 @@ V5 状态管理统一的后续阶段,针对两类高频性能问题进行细
| 4.1 | SidebarProvider memoize | `modules/layout/components/sidebar-provider.tsx` resize debounce 200ms + useMemo context + useCallback toggleSidebar |
| 4.2 | lesson-plan-editor selector 拆分 | `lesson-plan-editor.tsx` + 3 个子组件改为单字段 selector |
| 4.3 | 题库表 + 考试表虚拟化 | 新增 `@tanstack/react-virtual` 依赖;`question-data-table.tsx` + `exam-data-table.tsx` 引入 `useVirtualizer` |
| 4.4 | recharts props 模块级常量 | 14 个 chart 文件提取 `CHART_MARGIN`/`AXIS_TICK`/`TOOLTIP_CURSOR` 等模块级常量 |
| 4.4 | recharts props 模块级常量 | 12 个 chart 文件提取 `CHART_MARGIN` 模块级常量parent/grades/dashboard/homework 模块下) |
| 4.5 | useOptimistic 改造 | `message-detail.tsx` + `message-list.tsx` + `grade-record-list.tsx` + `attendance-sheet.tsx` 改用 `useOptimistic` + `useTransition` |
| 4.6 | 高频子组件 React.memo | `AssignmentCard` + `StatusBadge` + `EmptyState` 添加 React.memo(ClassCard/GraphKpNode 已有) |
| 4.7 | AiAssistantWidget 懒加载 | 新增 `modules/ai/components/ai-assistant-widget-inner.tsx`,外层改为 lazy wrapper |
| 4.8 | scan-image-viewer aspect-ratio | `modules/homework/components/scan-image-viewer.tsx` `<img>` 加 `aspectRatio: "4 / 3"` + width/height 属性 |
| 4.9 | metadata 增强 | `app/layout.tsx` 补 `metadataBase`/`openGraph`/`robots`/`applicationName`/`authors`/`creator` |
| 4.9 | metadata 增强 + Promise.all | `app/layout.tsx` 补 `metadataBase`/`openGraph`/`twitter`/`robots`/`applicationName`/`authors`/`creator`;`getLocale()`+`getMessages()`+`auth()` 串行 await 改 `Promise.all` 并行 |
### 未完成项(待后续专项)
- **React Compiler**:`babel-plugin-react-compiler` 仍为 RC 状态,暂不引入。待 React 19 稳定后评估。
- **force-dynamic 96 处评估**:全项目 96 处 `export const dynamic = "force-dynamic"`,需逐个评估页面数据变更频率,低频变更页面改 `revalidate = 3600`。
- **Playwright 性能断言**:待 e2e 测试基础设施完善后接入。
### 关键架构决策

View File

@@ -5,8 +5,8 @@
"generatedAt": "2026-06-17",
"formatVersion": "1.1",
"rule": "每次文件修改后须同步更新本文件",
"lastUpdate": "组件化重构专项全量完成2026-07-06含遗漏补全Phase 1 底座补全 + 三批 10 个模块闭环 + 遗漏补全 9 个模块。Phase 1新增 ErrorBoundary 基础类组件、StatsGrid 容器、SkeletonCard variant收敛 SectionErrorBoundary/RouteErrorBoundary/WidgetBoundary 为 preset。第 1 批grades/attendance/error-book/elective拆分 2 个巨型文件,迁移 12 个重复组件。第 2 批homework/exams/textbooks拆分 5 个巨型文件,迁移 3 个 filter + 1 个同名 error-boundary。第 3 批lesson-preparation/ai/dashboard拆分 2 个巨型文件,新增 DashboardShell迁移 6 个重复组件。遗漏补全classes/audit/school/settings/adaptive-practice/announcements/messaging/questions/student迁移剩余 20 个重复组件9 filters + 2 stats-cards + 5 skeletons + 4 error-boundaries补充 school/settings/classes 模块 i18n error.boundary* 键。总计9 个巨型文件全部 ≤500 行(最大 37549 个重复组件全部删除无残留modules/*/components/ 下无 *-filters/*-stats-card*/*-skeleton/*-error-boundary 文件tsc 零错误,lint 新增错误。",
"lastUpdated": "2026-07-06"
"lastUpdate": "性能预算重构专项 Phase 1-4 完成2026-07-05 审计 + 2026-07-07 补漏Phase 1 配置基线:(1.1) next.config.ts 新增 experimental.optimizePackageImports 覆盖 lucide-react/recharts/@xyflow/react/@tiptap/* /@radix-ui/*/date-fns(1.2) serverExternalPackages 追加 tencentcloud-sdk-nodejs + exceljs(1.3) providers.tsx 注入 WebVitalsReporter 闭环 RUM 上报链路;(1.4) lighthouse.yml + lighthouserc.json CI 性能门槛LCP<=3000ms/CLS<=0.1 error 级。Phase 2 Bundle 预算优化:(2.1) TipTap 三实例shared/ui/rich-text-editor + exams/editor/exam-rich-editor + lesson-preparation/blocks/rich-text-block全部改 next/dynamic ssr:false + xxx-inner.tsx 拆分模式;(2.2) KnowledgeGraph 改 ReactFlowProvider 同步壳 + next/dynamic lazy inner(2.3) exams/editor/types.ts barrel 减少服务端消费方拉入 @tiptap/* 运行时;(2.4) chart.tsx 改 recharts 具名导入ResponsiveContainer/Tooltip/Legend替代 import * as RechartsPrimitive barrel(2.5) status-badge 移除多余 use client(2.6) 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) 12 个 recharts 图表组件 margin props 提取到模块级 CHART_MARGIN 常量避免 inline 重建parent/grades/dashboard/homework 12 文件);(4.7) scan-image-viewer aspect-ratio(4.8) AiAssistantWidget inner 拆分 + 动态 import(4.9) layout.tsx metadata 增强metadataBase/openGraph/twitter/robots/authors/creator+ Promise.all 并行获取 locale/messages/session。架构决策(1) 动态 import 标准模式 xxx-inner.tsx + lazy wrapper(2) 请求级去重 > 跨请求 cacheReact cache() not unstable_cache(3) 批量 SQL > 循环 SQLINSERT batch + UPDATE CASE WHEN(4) React 19 useOptimistic 替代手动 isPending(5) 细粒度 Zustand selector > useShallow 多字段 selector(6) recharts 具名导入 + optimizePackageImports 替代 barrel 导入。未完成项React Compilerbabel-plugin-react-compiler RC 状态暂不引入、force-dynamic 96 处评估待后续专项、Playwright 性能断言(待 e2e 基础设施完善。验证npx tsc --noEmit 零新增错误npm run lint 新增错误。",
"lastUpdated": "2026-07-07"
},
"architectureOverview": {
"layers": [
@@ -27632,4 +27632,4 @@
}
}
}
}
}

View File

@@ -1140,22 +1140,41 @@ if (announcement.type === "school") {
- `src/modules/notifications/channels/in-app-channel.ts` — `sendBatch` 重写为单次批量 INSERT
- `src/modules/notifications/dispatcher.ts` — `sendBatchNotifications` 重构为 in_app 批量 + 其他渠道并行
## Phase 2 Bundle 预算优化规则2026-07-05
## Phase 1 配置基线规则2026-07-07 补漏
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| 启用 optimizePackageImports 优化 barrel 库 | `experimental: { optimizePackageImports: ["lucide-react", "recharts", "@xyflow/react", "@tiptap/*", "@radix-ui/*", "date-fns"] }` | 不配置(整个 barrel 被打入首屏 chunk |
| serverExternalPackages 覆盖所有 Node.js 原生模块 | `serverExternalPackages: ["mysql2", "tencentcloud-sdk-nodejs", "exceljs"]` | 仅 `["mysql2"]`(遗漏腾讯云 SDK 2-3 MB + Excel 250-300 KB |
| Web Vitals 上报组件必须注入到 Providers | `<WebVitalsReporter />` 在 Providers 内渲染(与 Toaster 同级) | 实现了组件但未注入(生产环境零 RUM 数据) |
| Lighthouse CI 门槛 LCP/CLS 为 error 级 | `assertions: { "largest-contentful-paint": ["error", { "maxNumericValue": 3000 }], "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }] }` | 全部用 warn性能回归不阻断合并 |
**Phase 1 涉及文件**
- `next.config.ts` — 新增 experimental.optimizePackageImports + 扩展 serverExternalPackages
- `src/app/providers.tsx` — 注入 `<WebVitalsReporter />`
- `src/app/web-vitals-reporter.tsx` — useReportWebVitals + sendBeacon 上报
- `src/app/api/web-vitals/route.ts` — edge runtime 端点 + 预算基线对比
- `.gitea/workflows/lighthouse.yml` + `lighthouserc.json` — CI 性能门槛
## Phase 2 Bundle 预算优化规则2026-07-05 + 2026-07-07 补漏)
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| 重型客户端库用 next/dynamic ssr:false | `dynamic(() => import("./xxx-inner").then(m => m.XxxInner), { ssr: false, loading: () => <Skeleton/> })` | 顶层静态 import 整个 TipTap/ReactFlow |
| Provider 上下文与懒加载实现分离 | 同步壳保留 `<ReactFlowProvider>`inner 用 `useReactFlow()` 钩子 | inner 直接调用 `useReactFlow()` 但未在同层 Provider 内 |
| forwardRef 组件动态导入需保留 forwardRef 外壳 | `export const ExamRichEditor = forwardRef((props, ref) => <LazyInner {...props} ref={ref} />)` | 直接 `export const ExamRichEditor = dynamic(...)`ref 丢失) |
| 服务端消费方仅需类型时拆 types barrel | `import type { EditorDoc } from "@/modules/exams/editor/types"`(纯类型入口) | `import { EditorDoc } from "@/modules/exams/editor/exam-rich-editor-types"`(拉入 @tiptap/* 运行时) |
| recharts 改具名导入替代 barrel | `import { ResponsiveContainer, Tooltip, Legend } from "recharts"` | `import * as RechartsPrimitive from "recharts"`Turbopack 难以 tree-shake |
| 纯展示组件无需 "use client" | `function StatusBadge({ status }) { return <span>...</span> }` | 在文件首行加 `"use client"` 但未使用任何客户端 hook |
| 角色路由必须配 loading.tsx | `app/(dashboard)/teacher/loading.tsx` 提供 Skeleton fallback | 仅依赖默认 Next.js 加载状态导致页面切换白屏 |
**Phase 2 涉及文件**
- `src/modules/exams/editor/exam-rich-editor.tsx` — 动态 import 化
- `src/modules/questions/components/question-rich-editor.tsx` — 动态 import 化
- `src/modules/lesson-preparation/components/paper-editor/paper-rich-editor.tsx` — 动态 import 化
- `src/modules/ai/components/ai-assistant-widget.tsx` — lazy wrapper + ai-assistant-widget-inner.tsx 拆分
- `src/modules/textbooks/components/knowledge-graph.tsx` + `knowledge-graph-inner.tsx` — ReactFlowProvider 同步壳 + lazy inner
- `src/shared/components/ui/rich-text-editor.tsx` + `rich-text-editor-inner.tsx` — TipTap 动态导入
- `src/modules/exams/editor/exam-rich-editor.tsx` + `exam-rich-editor-inner.tsx` — TipTap 动态导入forwardRef wrapper
- `src/modules/lesson-preparation/components/blocks/rich-text-block.tsx` + `rich-text-block-inner.tsx` — TipTap 动态导入
- `src/modules/ai/components/ai-assistant-widget.tsx` + `ai-assistant-widget-inner.tsx` — AI SDK 动态导入
- `src/modules/textbooks/components/knowledge-graph.tsx` + `knowledge-graph-inner.tsx` — ReactFlow 动态导入
- `src/shared/components/ui/chart.tsx` — recharts 具名导入替代 barrel
- `src/modules/exams/editor/types.ts` — 新建类型 barrel
- `src/shared/components/ui/status-badge.tsx` — 移除 "use client"
- `src/app/(dashboard)/teacher/loading.tsx`、`student/loading.tsx`、`parent/loading.tsx` — 新建 Skeleton fallback
@@ -1194,6 +1213,8 @@ if (announcement.type === "school") {
| next/web-vitals Metric 类型从签名推导 | `type M = Parameters<Parameters<typeof useReportWebVitals>[0]>[0]` | `import type { Metric } from 'next/web-vitals'`v15+ 已不导出) |
| PerformanceEntry 显式标注类型 | `metric.entries.map((entry: PerformanceEntry) => ({...}))` | `(entry) => ({...})`implicit any |
| sendBeacon 失败时回退 fetch keepalive | `if (!navigator.sendBeacon(url, blob)) { await fetch(url, { keepalive: true }) }` | 仅用 fetch页面卸载时可能被取消 |
| layout.tsx metadata 必须增强 SEO 字段 | `metadataBase: new URL(env.NEXTAUTH_URL ?? "http://localhost:3000"), openGraph: {...}, twitter: {...}, robots: {...}, authors: [...], creator: "..."` | 仅 `title` + `description`OG image 加载异常、SEO 缺失、build 时 warn |
| layout.tsx 串行 await 改 Promise.all | `const [locale, messages, session] = await Promise.all([getLocale(), getMessages(), auth()])` | 三次串行 `await`(增加 TTFB/FCP/LCP |
**Phase 4.1/4.3/4.4/4.6-4.9 涉及文件**
- `src/shared/components/ui/empty-state.tsx` — React.memo
@@ -1203,8 +1224,9 @@ if (announcement.type === "school") {
- `src/modules/exams/components/exam-data-table.tsx` — useVirtualizer
- `src/modules/layout/components/sidebar-provider.tsx` — resize debounce + useMemo
- `src/modules/homework/components/scan-image-viewer.tsx` — aspect-ratio
- 14 个 chart 组件(`src/modules/*/components/*-chart.tsx` — recharts props 提取模块级常量
- 12 个 chart 组件(parent/grades/dashboard/homework 模块下) — recharts margin 提取 CHART_MARGIN 模块级常量
- `src/app/web-vitals-reporter.tsx` — Metric 类型推导修复 + PerformanceEntry 注解
- `src/app/layout.tsx` — metadata 增强metadataBase/openGraph/twitter/robots/authors/creator+ Promise.all 并行获取 locale/messages/session
- `package.json` — 新增 `@tanstack/react-virtual`、`@tanstack/react-query-devtools`
## Phase 4.3 useOptimistic 乐观更新应用规则(补充)