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:
@@ -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 乐观更新应用规则(补充)
|
||||
|
||||
Reference in New Issue
Block a user