# 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` 挂在 ``。`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` + `