31b54aa71d
Self-hosted PostgreSQL is the runtime. A missing APP_DATABASE_URL must not tell users the retired hosted service is unconfigured. Co-authored-by: Cursor <cursoragent@cursor.com>
155 lines
9.4 KiB
Markdown
155 lines
9.4 KiB
Markdown
# PROGRESS · 前端优化九条(2026-08-28 / 2026-08-29)
|
||
|
||
工作树:`../.worktrees/frontend-optimization-20260828`
|
||
分支:`codex/frontend-optimization-20260828` @ `bf697d71` 之上
|
||
不要写成 `PROGRESS.md`:根目录已有受版本控制的 `progress.md`。
|
||
|
||
## 任务 0 · 基线复核
|
||
|
||
| 复核项 | 审计给的数 | 实测(`bf697d71` + 本轮前) | 结论 |
|
||
| --- | --- | --- | --- |
|
||
| `Home` 起止行 | `page.tsx:1090–4313` | `1090–4313` | 一致 |
|
||
| `Home` 行数 | 3,224 | 3,224 | 一致 |
|
||
| `Home` 内 `useState` | 61 | 61 | 一致 |
|
||
| `Home` 内 `useEffect` | 19 | 19 | 一致 |
|
||
| `Home` 内 `useMemo` / `useCallback` | 0 / 0 | 0 / 0 | 一致 |
|
||
| 全仓库 `React.memo` | 0 | 0 | 一致 |
|
||
| `globals.css` 行数 / 选择器数 | 3,278 / 1,389 | 3,278 行 / `{` 计数 1,453(选择器解析器不同,不以审计数为准) | 行数一致;选择器数用实测 |
|
||
| `!important` | 25 | 56 | 审计漏计;不以 25 为对照 |
|
||
| 字体文件 / `@font-face` / `next/font` | 0 / 0 / 0 | 0 / 0 / 0 | 一致 |
|
||
| `public/` 未引用 SVG | 4 | 5(`file/vercel/window/globe/next.svg` 均无产品引用) | 审计漏了 `next.svg` |
|
||
| `jyotish-logo.png` | 128×125 / 33,511 B | 同 | 一致 |
|
||
| `frontend/tests` 文件数 | 260 | 260 | 一致 |
|
||
| `tsx --test` | 待实测 | **2196 pass / 0 fail / 0 skipped**,约 292 s | 基线 |
|
||
| `./node_modules/.bin/tsc --noEmit` | 待实测 | 先红后绿:测试夹具类型(未改断言语义)修完后 exit 0。不要用 `npx tsc`(会装到 `tsc@2.0.4` 空包) | 基线 |
|
||
| `next build` | 待实测 | exit 0,约 13–22 s;当时**全部路由 `ƒ Dynamic`** | 基线 |
|
||
| `/` 首屏 JS / CSS gzip | 待实测 | JS 16 chunk / **438.5 KB**;CSS 2 chunk / **33.7 KB**(globals 约 32.9 KB) | 基线 |
|
||
|
||
差异说明:审计没跑过 `next build`。根 `force-dynamic` 让整棵树都是 `ƒ`。`npx tsc` 在本环境会装错包,验收一律用 `./node_modules/.bin/tsc --noEmit`。
|
||
|
||
## 任务 1 · 渲染基准(门控,通过)
|
||
|
||
脚本:`frontend/scripts/home-streaming-render-benchmark.mts`
|
||
合同测试:`frontend/tests/home-streaming-render-split.test.ts`
|
||
|
||
`renderToString` 不会证明 `React.memo`(SSR 每次走完整树)。基准测的是**拆分后的工作量**:已完成列表渲染 1 次 + 每个 token 只渲染流式行,对比整表每个 token 重建。反向验证就是改跑 `UnsplitChatTranscript`。
|
||
|
||
预热一次后再测两次。回复 **916 字**、**46 token**。
|
||
|
||
| 场景 | 未拆分 run1 | 未拆分 run2 | 时间偏差 | 拆分后 | 时间收益 | 列表渲染 |
|
||
| --- | ---: | ---: | ---: | ---: | ---: | --- |
|
||
| 空会话(1 条用户问题 + 流式) | 1.74 ms / 92 行渲染 | 2.16 ms / 92 行 | 23.6% | 1.60 ms / 1 已完成行 + 46 流式行 | 18% | 未拆分 46 次 vs 拆分 1 次 |
|
||
| 长会话(≥40 条历史,末条为用户) | 155.7 ms / 1932 行 | 147.3 ms / 1932 行 | **5.4%** | 4.58 ms / 41 已完成行 + 46 流式行 | **97.0%** | 未拆分 46 次 vs 拆分 1 次 |
|
||
|
||
渲染次数两次完全一致。长会话时间偏差 5.4% < 20%,收益 97% >> 20%。空会话时间偏噪、收益不足 20%,验收以长会话为准。`usable: true`。
|
||
|
||
## 任务 2 · 五条清理
|
||
|
||
### 2.1 删除根 `force-dynamic` — 交付
|
||
|
||
改后 `next build` 路由模式:
|
||
|
||
- `○ Static`:`/`、`/_not-found`、`/membership`、`/membership/orders`
|
||
- 仍为 `ƒ Dynamic`:`/login`、`/admin/**`、`/reports`、`/reports/[reportId]`、全部 API
|
||
|
||
`/` 静态化说得清:根布局不读 `cookies()`/`headers()`,页面是 client,HTML 壳可预渲染。`next.config.ts` 仍对 `/` 和 `/login` 设 `private, no-store`。`/login` 自己读 `headers()`。会员页数据在客户端拉,静态壳可接受。
|
||
|
||
**既有断言改动(锁的是错误实现,不是“聊天 HTML 不缓存”):**
|
||
`frontend/tests/stale-client-recovery.test.ts` 不再要求根布局 `force-dynamic`,改为禁止根布局再写这行,并锁 login/reports/admin 仍各自声明。反缓存合同仍是 `next.config.ts` 的 `Cache-Control`。
|
||
|
||
### 2.2 Inter — 选 (a)
|
||
|
||
`next/font/google` Inter,`display: "swap"`,`--font-inter` 挂在 `<html>`。`DESIGN.md` §3 已记。`/` HTML 有 woff2 `rel="preload" as="font"`。未加载 Tiempos。建档 BUG-430。
|
||
|
||
### 2.3 `web/*.html` — 不删(止损/登记)
|
||
|
||
`scripts/jyotish_api_server.py` 仍提供 `/`、`/evidence`、`/rectification`;`tests/test_api_async_job_contract.py` 锁路径。任务书检索不全。移走会改 `tests/**` 既有断言。见 `BLOCKED.md`。
|
||
|
||
### 2.4 public 资源
|
||
|
||
删除 5 个无引用 SVG。logo 按 2×(约 64px)重导出并 `optimize=True`:64×62 / **8,584 B**(原 33,511 B)。未换成 SVG(三维渲染)。`email-otp-login` 仍 `next/image` `alt=""`。
|
||
|
||
### 2.5 合并 effect
|
||
|
||
一个 effect、三个显式分支。建档 BUG-429。新增测试,未改 `chart-library-other-profile` 既有断言。
|
||
|
||
## 任务 3 · 消息列表拆分 — 交付
|
||
|
||
`ChatTranscript` = `SettledMessageList`(memo)+ `StreamingMessageEntry`(memo)。流式期间历史 props 不含 token 文本。禁止手写 `useMemo`/`useCallback`;handler 走 `actionsRef`。
|
||
|
||
长会话收益见任务 1(97% 时间、列表渲染 46→1)。反向:`UnsplitChatTranscript` 回到 155 ms / 1932 行。
|
||
|
||
**既有断言改动:**
|
||
|
||
- `chat-navigation-a11y-contract`:原先锁 `page.tsx` 内联 `chatMessageViews(`,那正是本条要拆掉的热点。无障碍合同改为锁 `aria-busy` + `<ChatTranscript`;列表切片仍不得出现 `aria-live`/`role="status"`(`role="alert"` 的错误行仍在 `page.tsx`)。
|
||
- `chat-stream-layout`:`<ChatMessageActions` 改锁 `chat-transcript.tsx`。合同是“普通咨询复用共享操作条”,不是 JSX 必须写在 `page.tsx`。
|
||
- `consultation-entrypoint`「已回答对话不再给输入框上方建议」:`ConversationFollowUps` / `deriveConsultationFollowUps` 改锁 transcript。合同是追问在答案下方而不是 composer chips。
|
||
|
||
未达 20% 才会还原;未触发。
|
||
|
||
## 任务 4 · Home 第一刀(账户弹层)— 交付
|
||
|
||
`AccountDialogOverlay`:`open === false` 时 props 稳定(`openEpoch` 固定为 0),memo 跳过。打开时从 `modelRef.current` 读最新模型。`chartLibraryOpen` 下沉到 overlay。
|
||
|
||
- `Home` `useState`:**61 → 60**
|
||
- `Home` `useEffect`:19 → 18(含任务 2.5)
|
||
- 未碰生时校正外壳
|
||
- DESIGN.md 弹层契约:关闭按钮 `aria-label="关闭"`、dialog `aria-modal`、错误/成功 live region、44px 触达类名未改
|
||
|
||
弹层关闭时的流式收益无法用 SSR 量化(关闭路径是 `return null`)。反向:`frontend/tests/account-dialog-overlay.test.ts` 证明 `open=false` 不调用 `renderProfile`/`renderLogout`。未触发 400 行硬停(overlay 组件约 80 行;资料 JSX 仍在 `page.tsx` 的 render 函数里以保住源码锁)。
|
||
|
||
## 任务 5 · admin 与 globals.css 隔离 — 交付
|
||
|
||
根布局不再 import `globals.css`。站点路由 import `site-styles.ts`。`error.tsx` / `not-found.tsx` **不能** import(它们与 admin 共享根段,会把 33 KB globals 打进 admin)。
|
||
|
||
`/admin/users|orders|roles|feature-flags` 客户端 CSS 清单:
|
||
|
||
| | 基线(任务 0,全树 ƒ) | 本轮 |
|
||
| --- | --- | --- |
|
||
| `/` CSS gzip | 33.7 KB | 34.5 KB(含 Inter `@font-face`) |
|
||
| `/admin/*` CSS gzip | globals 32.9 KB + admin 1.7 KB ≈ **34.6 KB** | admin 1.8 KB + Inter 0.8 KB = **2.5 KB** |
|
||
|
||
约 **−32 KB gzip**。未登录,未能目视后台四页;用构建清单代替。见 `BLOCKED.md`。
|
||
|
||
`/` 现为 `○ Static`,HTML 可预渲染。`/` JS gzip 458.2 KB(基线 438.5 KB,+Inter +transcript)。
|
||
|
||
## 本轮后 `Home` 数字
|
||
|
||
| | 任务 0 | 现在 |
|
||
| --- | ---: | ---: |
|
||
| 行数 | 3,224 | 3,231 |
|
||
| `useState` | 61 | 60 |
|
||
| `useEffect` | 19 | 18 |
|
||
| `useMemo`/`useCallback` | 0/0 | 0/0 |
|
||
| `React.memo`(`frontend/src`) | 0 | transcript + overlay |
|
||
|
||
## 既有测试断言改动(仅上述三处)
|
||
|
||
理由见 2.1 与任务 3。其余 `frontend/tests/**` 与 `tests/**` 未改语义。类型夹具 `as const` / `Object.assign` 只为 `tsc`(BUG-409),不断言行为。
|
||
|
||
## 止损
|
||
|
||
- 2.3:不删 `web/*.html`
|
||
- 3/4:未触发 <20% 还原
|
||
- 5:源码隔离后若 `error`/`not-found` 仍 import globals,admin 会拿回 33 KB;已从这两个边界拿掉
|
||
- 后台登录目视:阻塞
|
||
|
||
## 第 09 条 · 深色主题(待决策,人拍板)
|
||
|
||
`globals.css` 是 `color-scheme: light`,0 处 `prefers-color-scheme`。DESIGN.md 把纸感暖白写成产品身份,但 23,000 字文档没写“永远不做深色”。**这是产品决策,不是本轮代码任务。** 需要人决定:维持单一浅色,或另开一轮做深色主题。
|
||
|
||
## 最终测试数
|
||
|
||
基线 **2196** → 本轮 **2205 pass / 0 fail / 0 skipped**(+9:chart-library-session 5、home-streaming-render-split 1、site-style-isolation 2、account-dialog-overlay 1)。
|
||
|
||
浏览器(`next start` PORT 3010,未注入 `AUTH_PROVIDER=self-hosted` 与本地 PostgreSQL URL):`/login` 可见压缩 logo 与 Inter 变量 class、Link 头 `woff2` preload;`/membership` 站点样式完整;`/admin/users` 仍 403。未登录 `/` 因本机没有业务库连接而走恢复页,不是本轮回归。运行时是 Better Auth + 自维护 PostgreSQL,不是 Supabase。后台已登录四页目视仍见 `BLOCKED.md`。
|
||
|
||
## 验收命令(本机)
|
||
|
||
```bash
|
||
./node_modules/.bin/tsc --noEmit
|
||
./node_modules/.bin/eslint # 0 error
|
||
./node_modules/.bin/tsx --test tests/*.test.ts
|
||
./node_modules/.bin/next build
|
||
```
|