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>
9.4 KiB
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。
HomeuseState:61 → 60HomeuseEffect:19 → 18(含任务 2.5)- 未碰生时校正外壳
- DESIGN.md 弹层契约:关闭按钮
aria-label="关闭"、dialogaria-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。
验收命令(本机)
./node_modules/.bin/tsc --noEmit
./node_modules/.bin/eslint # 0 error
./node_modules/.bin/tsx --test tests/*.test.ts
./node_modules/.bin/next build