# M16 / issue-01~05：Monorepo 性能治理首轮实现

## 背景

- 当前 `apps/web` 与 `apps/admin` 在开发态存在明显冷启动与首编译压力：
  - `web /` 首次进入编译链路较长，技能区静态引入 `echarts`
  - `admin /dashboard/resume` 与 `/dashboard/ai` 会直接拉起重型编辑器与 AI 面板
- `apps/server` 公开读取接口缺少明确缓存策略，交付层表达还不够完整。
- 在进入更深层的 UI libs 拆分前，需要先把性能观测和低风险收益点建立起来。

## 本次目标

- 建立可重复执行的性能基线脚本
- 收口 `web / admin` 的低风险按需加载边界
- 优化开发脚本的首编译体验
- 为 `server` 的公开 / 私有简历接口补上缓存策略

## 实际改动

### 1. 根级性能脚本

- 新增 `scripts/perf-dev-route.mjs`
  - 用于采集 dev 服务的首个可访问时间、首次响应时间和二次访问时间
- 新增 `scripts/perf-build-analyze.mjs`
  - 用于汇总 `apps/web/.next` 与 `apps/admin/.next` 的静态资源体积和 route manifest
- 根 `package.json` 新增：
  - `pnpm perf:dev:web`
  - `pnpm perf:dev:admin`
  - `pnpm perf:build:analyze`
- `.gitignore` 新增 `.tmp`

### 2. Dev 冷启动与首编译优化

- `apps/web/package.json`
  - `dev` 切到 `next dev --turbopack --port 5555`
- `apps/admin/package.json`
  - `dev` 切到 `next dev --turbopack --port 5566`
- `apps/web/next.config.ts`
  - 开启 `experimental.optimizePackageImports`，优先收 HeroUI / Iconify 的导入开销
- `apps/admin/next.config.ts`
  - 开启 `experimental.optimizePackageImports`，优先收 HeroUI 的导入开销

### 3. Web 首屏重依赖按需化

- 新增 `apps/web/components/published-resume/published-resume-skills-chart.tsx`
  - 将 `echarts` 的运行时代码从技能区主组件拆出
- `apps/web/components/published-resume/published-resume-skills-section.tsx`
  - 改为客户端挂载后再动态 `import('./published-resume-skills-chart')`
  - 保留带 `aria-label` 的轻量 fallback，避免交互和测试回归

### 4. Admin 重工作区加载链路优化

- `apps/admin/components/admin/ai-workbench-shell.tsx`
  - 将 `AiFileExtractionPanel`
  - `AiAnalysisPanel`
  - `AiCachedReportsPanel`
  - 全部改为动态加载
- `apps/admin/components/admin/resume-shell.tsx`
  - 将 `ResumeDraftEditorPanel` 改为动态加载
- `apps/admin/app/dashboard/ai/page.tsx`
- `apps/admin/app/dashboard/resume/page.tsx`
- `apps/admin/app/dashboard/publish/page.tsx`
  - 页面入口继续增加轻量 loading 边界，避免首屏直接拉满重模块

### 5. Server 交付层优化

- `apps/server/src/main.ts`
  - 显式启用 `Express etag = strong`
- `apps/server/src/modules/resume/resume.controller.ts`
  - `GET /resume/published`：增加公开缓存头
  - `GET /resume/published/summary`：增加公开缓存头，并按 cookie 变化设置 `Vary`
  - `GET /resume/published/export/markdown`
  - `GET /resume/published/export/pdf`
    - 增加公开缓存头
  - `GET /resume/draft`
  - `GET /resume/draft/summary`
    - 增加 `private, no-store` 头，避免私有内容缓存污染
- 新增 `apps/server/src/modules/resume/__tests__/resume.controller.spec.ts`
  - 覆盖公开缓存与私有 no-store 两类头

## Review 记录

- 低风险优先：
  - 保留 HeroUI / ECharts，不做库替换
  - 先做 Turbopack、动态边界和缓存头，不在本轮强推共享包产物化
- 教学节奏优先：
  - 先补可观测脚本，再做优化，避免只有“体感变快”
  - `summary` 与主 DTO 不改 shape，本轮只动加载链路和交付层

## 遇到的问题

- `apps/server` 在 `main.ts` 中获取 Express 实例时，Nest 的 `getInstance()` 不能直接写泛型，后改为显式类型断言。
- `apps/admin` 全量测试中已有 `components/resume/__tests__/draft-editor-panel.spec.tsx` 两条历史失败：
  - 1 条超时
  - 1 条保存次数断言为 2 次
- 这两条失败不在本轮改动文件范围内；与本轮新增的性能边界无直接耦合，因此先记录，不在当前 issue 扩 scope 修复。

## 测试与验证

### 已通过

- `pnpm --filter @my-resume/web typecheck`
- `pnpm --filter @my-resume/admin typecheck`
- `pnpm --filter @my-resume/server typecheck`
- `pnpm --filter @my-resume/web test`
- `pnpm --filter @my-resume/server test`
- `pnpm --filter @my-resume/admin exec vitest run components/admin/__tests__/dashboard-shell.spec.tsx components/admin/__tests__/ai-workbench-shell.spec.tsx`
- `node scripts/perf-build-analyze.mjs --apps web,admin --output .tmp/perf/build-report.json`

### 待后续继续跟进

- `pnpm --filter @my-resume/admin test`
  - 当前被既有 `draft-editor-panel` 两条失败阻塞，需要后续单独 issue 收口
- `pnpm --filter @my-resume/web build`
- `pnpm --filter @my-resume/admin build`
  - 本轮已启动验证，可继续在后续 perf issue 中记录 build 体积对比结果

## 后续可写成教程 / 博客的切入点

- 为什么 Next dev 冷启动要先做“观测脚本”，再做优化
- 如何用“动态边界”处理 `echarts`、大型编辑器、AI 工作台这类重模块
- monorepo 中 `transpilePackages`、`optimizePackageImports` 与共享包开发体验之间的权衡
- 为什么公开接口和私有接口要使用不同的缓存策略
