docs(frontend): task book for the nine-item web optimization pass

Derived from a static audit of frontend/ and web/ at staging 87f32d59.
The audit ran without node_modules, so no build, eslint, or tsc was
executed; every size and timing claim is inferred and task 0 requires
the executing agent to re-measure before acting on it.

Sequencing follows the repository's own history. The React Compiler
route stays closed per PROGRESS-react-compiler-20260817.md and
BLOCKED.md: Home fails on upstream compiler Todo/Invariant errors, not
on repository code, and neither compiler version raised the
project-wide success count above 134. A render benchmark gates the two
performance tasks, because the same missing benchmark already blocks
the 44-component decision recorded in BLOCKED.md.

Dark mode is left out as a product decision rather than an agent task.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVapmh2oGNyr6ECHKjPJY8
This commit is contained in:
Jesse_Chen
2026-08-28 18:38:53 +00:00
parent 87f32d596f
commit bf697d71a2
+305
View File
@@ -0,0 +1,305 @@
# TASK · 前端优化九条(codex/frontend-optimization-20260828
## 来源与前提
本任务书来自一次前端静态审计,范围 `frontend/``web/`,基线 `staging @ 87f32d59`
**审计方自己的边界(必须先知道):** 审计时 `frontend/node_modules` 是空的,**没有跑过 `next build` / `eslint` / `tsc`**。
下面所有数字都来自源码静态检索、可复核,但凡涉及**产物体积**与**运行时耗时**的结论都是推断。
**任务 0 的第一件事就是复核这些数字,对不上以你实测的为准,并在进度文档里写明差异**——不要拿着一份没验证过的清单开工。
---
## 硬红线
1. **不得手写 `useCallback` / `useMemo` 去凑性能。** 本轮的手段是**拆组件 + `React.memo` 边界**。
理由见 `PROGRESS-react-compiler-20260817.md`:手写记忆化在这个仓库里已经被明确排除,
拆分才是同时解决重渲染和解锁编译器的路径。
2. **不得为了迎合 React Compiler 去改 `page.tsx` 的写法。** 那条路已经走到底并证伪(见下方"已关闭的路")。
3. **不得在未跑 `npx tsc --noEmit` 的情况下推 `staging`。** 这是 BUG-409 的防复发条款:
validate 阶段跳过 `next build`,类型错误要到 publish 才炸,公网会停在上一个 SHA。`tsx --test` 全绿不能替代。
4. **测试数只许 ≥ 任务 0 实测的基线,且 `skipped=0`、`fail=0`。** 不得删测试、不得 skip 测试来换绿。
5. **不得改 CI 配置**`.gitea/``.github/`)。
6. **不得动 `frontend/tests/**` 与 `tests/**` 里的既有断言**;新增测试可以,改既有断言不行——除非你能证明该断言锁的是错误行为,并在进度文档里单独论证。
7. **不得在存在未提交修改的工作树上切分支 / stash / reset / 顺带提交用户变更。**
## 让步顺序
功能与测试不回归 **>** 可被验证的性能收益 **>** 代码整洁度 **>** 构建速度。
任何一步做不到时,**写进 `BLOCKED.md` 并停在那一步**,不要绕路硬做。本仓库已经有过一次
"配置开了、构建绿了、实际零收益"的教训,宁可少交付也不要交付看起来完成的东西。
## 已关闭的路(别重跑)
`PROGRESS-react-compiler-20260817.md``BLOCKED.md` 记录了完整调查:
`Home` 无法被 React Compiler 编译,失败全部来自上游编译器的 `Todo:`(未实现语法)与 `Invariant:`(编译器内部断言,即编译器 bug),
**本仓库代码没有写错**。稳定版 1.0.0 与 experimental 版的全项目编译成功数都是 **134**,一个没多。
所以:**不要再去升编译器版本、不要再去装 `babel-plugin-react-compiler` 做诊断、不要再开 `reactCompiler: true` 试。**
那份文档给出的下一步就是本任务书的任务 4。
---
## 开工前置
`AGENTS.md` 第 5、6、7 节执行,不可省略:
```bash
git fetch origin --prune
git worktree add -b codex/frontend-optimization-20260828 \
../.worktrees/frontend-optimization-20260828 origin/staging
```
- 基线必须是 **`origin/staging`**,不得基于本地 `staging` 或本地 `main`
-`docs/research/pre_work_error_ledger.md`
-`python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45`
- **读 `frontend/AGENTS.md`**:这个 Next.js 版本与你训练数据里的不一样,
写任何 Next.js 代码前先读 `frontend/node_modules/next/dist/docs/` 里的对应指南。任务 3 尤其吃这一条。
- 本任务书涉及"重渲染、样式加载、字体不生效"等现象,按第 7 节先检索 `docs/BUG_HISTORY.md`
(关键词建议:`重渲染``force-dynamic``globals.css``字体``localStorage``chartLibrary`),
命中记录的防复发措施逐条确认是否仍然存在。
---
## 任务 0 · 基线复核(必做,不产出代码)
`npm install`worktree 是新的,没有 `node_modules`),然后逐项实测并填表:
| 复核项 | 审计给的数 | 你的实测 | 结论 |
| --- | --- | --- | --- |
| `Home` 起止行 | `page.tsx:10904313` | | |
| `Home` 行数 | 3,224 | | |
| `Home``useState` | 61 | | |
| `Home``useEffect` | 19 | | |
| `Home``useMemo` / `useCallback` | 0 / 0 | | |
| 全仓库 `React.memo` | 0 | | |
| `globals.css` 行数 / 选择器数 | 3,278 / 1,389 | | |
| 字体文件数 / `@font-face` / `next/font` | 0 / 0 / 0 | | |
| `public/` 未引用 SVG | 4 | | |
| `jyotish-logo.png` 尺寸 / 体积 | 128×125 / 33,511 B | | |
| `frontend/tests` 测试文件数 | 260 | | |
| `tsx --test` 测试数 / 通过数 | 待实测 | | |
| `npx tsc --noEmit` | 待实测 | | |
| `npx next build` 退出码 / 耗时 | 待实测 | | |
| `/` 首屏 JS chunk 数 / gzip 体积 | 待实测 | | |
最后四行是**任务 2、5 的收益对照基准**,必须记下来,否则后面无法证明改动有收益。
---
## 任务 1 · 建渲染基准(**门控任务,不通过则任务 3、4 不许开始**)
**这是本轮最重要的一步。** 审计报告第 01、02 条的因果链是从代码结构推出来的,逻辑成立,
但"流式回复期间每个 token 重算多少毫秒"**没有实测过**。`BLOCKED.md` 里那 44 个组件的收益
也正是因为缺渲染基准而无法拍板——**同一个缺口卡住了三处决策,补一次能全解开。**
要求:
1. 建立一个可重复的渲染基准,测量**流式回复期间 `Home` 的重渲染次数与累计渲染耗时**。
手段自选(React Profiler API / `react-scan` / 在 `Home` 里挂一个仅开发期生效的计数器均可),
但必须满足:**同一份输入跑两次,结果偏差在可接受范围内**,否则它证明不了任何事。
2. 场景至少两个:
- **空会话**:新建会话 + 一条约 800 字的流式回复。
- **长会话**:≥ 40 条历史消息 + 一条约 800 字的流式回复。
长会话是第 02 条 `O(消息数) × 每 token` 的判定场景,缺了它任务 3 就没有验收依据。
3. 基准脚本与原始读数落到 `frontend/tests/``frontend/scripts/` 下,
并在进度文档里贴出**改动前的读数**。
**止损条款:** 如果两次运行的偏差大到无法区分 20% 的改进,
说明基准不可用——**把它写进 `BLOCKED.md` 并停止任务 3、4**,只交付任务 2 和任务 5。
不要在没有基准的情况下做性能改动然后声称有收益,这正是上一轮踩过的坑。
---
## 任务 2 · 低风险清理批次(可独立交付)
五条互不相关的机械修复,一个提交批次。**每条都要单独验证,不要打包声称"都改好了"。**
### 2.1 删除根布局的 `force-dynamic`
`frontend/src/app/layout.tsx:8``export const dynamic = "force-dynamic"` 是多余的:
`/login``/admin``/reports``/reports/[reportId]` **各自都已经声明过一遍**
根部这行只是让整棵路由树永久失去静态优化与路由级缓存。
- 根布局本身不读 `cookies()` / `headers()`(已核实),`page.tsx``"use client"` 且数据全在客户端拉。
- **但这条必须实测验证,不能想当然**:改动后跑 `npx next build`
逐条比对每个路由的渲染模式(`○ Static` / `ƒ Dynamic`),
确认**没有任何一个本该动态的路由被静态化**。尤其检查 `/`
`next.config.ts` 已对 `/``/login` 设了 `private, no-store`,但静态化后的行为要亲自确认。
- **止损条款:** 只要有一个路由的渲染模式变化说不清,**还原这一条**,其余四条照常交付。
### 2.2 `Inter` 声明了却从未加载
`frontend/src/app/globals.css:92``frontend/src/components/admin/admin-app.tsx:54` 都把 `Inter`
写进字体栈,但仓库里 0 个字体文件、0 处 `@font-face`、0 处 `next/font`——
Windows / Linux 用户一直静默落到 Segoe UI / 微软雅黑。
**注意区分:** `Tiempos Headline` 的缺席是**有据的取舍**`DESIGN.md` 第 3 节写明了授权文件不可用、
宋体栈是声明的生产替代。**不要动它。** 只处理 `Inter`
二选一,不许停在中间:
- **(a)** 用 `next/font/google` 真的加载 `Inter`(顺带拿到 preload 与 `font-display: swap`),
两处引用都要覆盖;
- **(b)** 从两处字体栈里删掉 `Inter`,让声明与渲染结果一致。
选哪个都要**在 `DESIGN.md` 第 3 节记一行**,说明这是一个决定而不是遗漏。
选 (a) 时必须实测确认字体确实被加载(Network 面板或构建产物里能看到 woff2),
否则等于把一个静默回退换成另一个。
### 2.3 删除 `web/` 下三个无人引用的 HTML
`web/index.html``web/rectification.html``web/evidence_packet.html`
全仓库检索不到引用,`mcp_server.py` 也没有挂载静态目录。它们把 API 响应直接
`JSON.stringify` 打印给用户,主色 `#006b6b` 与现行设计系统无关,是更早的原型残留。
- **删除前必须自己再检索一遍**(含 `deploy/``.gitea/``Dockerfile``docs/`),确认真的没有引用。
- 若确实还当调试夹具用,**移到 `scripts/` 下并在文件头写明用途**,不要留在 `web/` 冒充产品界面。
### 2.4 清理 `public/` 脚手架资源与 logo 体积
- 删除 `vercel.svg``window.svg``file.svg``globe.svg``create-next-app` 默认资源,无引用)。
`next.svg` 的检索命中疑似误报(`grep``.` 通配),**删前逐个确认**。
- `jyotish-logo.png` 实际 **128×125 却有 33,511 B**,登录页以 32px 显示。
同尺寸 PNG 合理体积在 4–6 KB。换 SVG,或按 2× 需求重导出并压缩。
换 SVG 时注意 `email-otp-login.tsx:285` 用的是 `next/image``alt=""`(装饰性,`DESIGN.md` 第 81 行有据),保持不变。
### 2.5 合并两个重叠的 effect
`frontend/src/app/page.tsx:1302``page.tsx:1338` 依赖数组都是 `[accountId, profile]`
都调用 `upsertSelfChart` 并写 `chartLibraryStorageKey`
`chartLibraryLoadedAccount` ref 提前 return 来分工,逻辑能跑通但没有注释。
代价是每次 `profile` 引用变化都在主线程同步做一次 `JSON.stringify` + `localStorage.setItem`
- 合并为一个 effect,用**显式分支**表达"首次加载云端"与"本地 upsert"两条路径。
- **行为必须完全等价**:云端拉取失败时本地库仍可用、`self` 记录仍被 upsert、
切换账号时仍清空。这三条各补一个针对性测试。
- 这块会随任务 4 的拆分一起被搬走,现在做掉能让后面那一刀更干净。
**任务 2 验收:** `npx tsc --noEmit` 无输出、`npx eslint` 无 error、
测试数 ≥ 基线且 `skipped=0``npx next build` 退出码 0。
---
## 任务 3 · 消息列表记忆化(依赖任务 1 通过)
`frontend/src/app/page.tsx:4010` 在 JSX 里直接调用
`chatMessageViews(activeSession.messages, …).map(…)`
结合 `Home` 的零记忆化,长会话下这是 **O(消息数) × 每个 token** 的整表重建。
`renderKey` 用得是对的,React diff 会剪掉大部分 DOM 写入,但 `chatMessageViews`
本身的计算与数组分配躲不掉。
要求:
1. 把消息列表提成独立组件并用 `React.memo` 包住。
2. **把"已完成的历史消息"与"正在流式的最后一条"拆成两个渲染单元**——
只有后者需要跟着 token 走。这是本条的核心,只加 `memo` 不拆是无效的。
3. 注意 `chatMessageViews` 的入参里有 `isLoading``activeStreamingText` 等会随 token 变的值,
拆分时要保证**历史消息那一半的 props 在流式期间引用稳定**,否则 `memo` 形同虚设。
**验收取证(必须可被推翻):** 用任务 1 的长会话场景,给出改动前后的
**重渲染次数与累计渲染耗时**两组读数。并做一次**反向验证**
临时去掉 `memo`,确认读数退回改动前水平——
这一步是为了证明你的取证方法不是恒为真。上一轮的教训就在这里。
**止损条款:** 若长会话场景下改进不足 20%,说明瓶颈不在这里,
**如实写进进度文档,还原改动**,不要为了交付而保留一个没有收益的重构。
---
## 任务 4 · 拆 `Home` 第一刀(依赖任务 1 通过;**只切一块**)
`page.tsx:10904313` 是一个 3,224 行的客户端组件,61 个 `useState`、19 个 `useEffect`
0 处记忆化。流式回复期间每个 token 都让整个函数体重跑一遍,
包括 `page.tsx:1200` 那串 `sessions.filter().filter().sort()`
以及侧边栏、会话列表、输入框、全部弹层的重渲染。
**本轮只切耦合最低的一块,不要试图拆完。** 建议优先级:
1. **账户 / 资料弹层**(耦合最低,state 集中,UI 边界清晰)
2. 图库 + 合盘
3. 生时校正外壳(耦合最高,涉及 `resumeRectificationSession` ref 与多个 effect**本轮不要碰**
要求:
- 切出去的子树用 `React.memo` 封边界,props 引用保持稳定。
- **`Home``useState` 数量必须实际下降**,并在进度文档里给出前后数字。
如果拆完 state 还留在 `Home` 里靠 props 往下传,那这一刀没有意义。
- 不得改变任何用户可见行为。`DESIGN.md` 里该组件的无障碍契约
44px 触达、`focus-visible`、live region、reduced-motion、forced-colors**逐条保留**——
这些在拆组件时最容易被顺手丢掉,交付前对着 `DESIGN.md` 第 4 节复核一遍。
**验收取证:** 同任务 3,给出改动前后读数 + 反向验证。
**止损条款(硬):** 单块拆分超出 **400 行改动**或触碰到第三类(生时校正外壳)时,
**立刻停下并把已完成的部分交付**,剩余写进 `BLOCKED.md`
这个组件已经有一轮失败史,宁可切一小块并证明收益,也不要开一个收不了口的大重构。
---
## 任务 5 · 隔离 admin 的样式负担
`globals.css` 3,278 行、1,389 个选择器、25 处 `!important`,由根布局引入,每条路由都拿全量。
`/admin` 最冤:它用 antd 组件树,**另外还要再加载 antd reset 与 antd 本体**
而这 3,278 行聊天 / 生时校正 / 会员页样式它几乎一条都用不上。
**本轮只做最小动作:** 把 admin 从根布局的 `globals.css` 里摘出去(给它一条自己的布局链)。
按现有的 8 个注释分区切成 shell / chat / rectification / membership 四个入口是**下一轮的事,本轮不做**。
- 摘出去后**必须逐页目视验证 admin 各路由没有样式塌陷**
`/admin/users``/admin/orders``/admin/roles``/admin/feature-flags` 至少各看一眼)。
admin 可能隐性依赖了 `globals.css` 里的某些 token 或 reset——
真依赖了就把那部分显式搬进 `admin.css`,不要把整个 `globals.css` 搬回去。
- **验收取证:** 给出 `/admin` 首屏 CSS 体积的改动前后对比(来自任务 0 记下的 build 输出)。
**止损条款:** 出现说不清的样式塌陷就还原,写进 `BLOCKED.md`
---
## 不在本轮范围
- **深色主题(审计第 09 条)。** `globals.css``color-scheme: light`、0 处 `prefers-color-scheme`
单一浅色主题完全可能是刻意选择——`DESIGN.md` 把这套视觉定义为"私人阅读室",纸感暖白是它的立身之本。
问题是 23,000 字的设计文档通篇没提这件事。
**这是产品决策,不是代码任务,需要人来拍板,agent 不要自行开工。**
本轮唯一动作:在进度文档里把这个待决策项列出来,交给决策者。
- **`globals.css` 的完整分包**(任务 5 的下一步)。
- **`Home` 的完整拆分**(任务 4 的第 2、3 块)。
---
## 收尾
1. **`docs/BUG_HISTORY.md`**:任务 2.5(重叠 effect)与任务 2.2(字体静默回退)属于第 7 节定义的
"异常 / 回归"范畴,需要建档。
**写之前先检索当前最大编号**——本地看到的是 `BUG-428`,但 `origin/staging` 可能已经推进并占用了后续号,
BUG-253 / BUG-255 记录过两次抢号失效。以远端实际最大编号顺延,并在记录里注明。
2. **进度文档**:新建 `PROGRESS-frontend-optimization-20260828.md`**不要写 `PROGRESS.md`**
根目录已有受版本控制的 `progress.md`,且文件系统大小写不敏感,会覆盖清单外的文件——
这个坑 `PROGRESS-react-compiler-20260817.md` 开头记过)。
内容:任务 0 的复核表、每个任务的取证读数与反向验证结果、止损触发情况、第 09 条待决策项。
3. **`BLOCKED.md`**:所有触发止损的任务,按既有格式追加。
4. **提交前**`npx tsc --noEmit` 无输出、`npx eslint` 0 error、
测试数 ≥ 基线且 `skipped=0` / `fail=0``npx next build` 退出码 0。
5. **推送**`git push origin HEAD:staging` 是快进推送,会触发 `backend-quality-gate`
**没有路径过滤,任何改动(包括纯文档)都会跑完整构建与部署**——
所以**同批改动合并成一次推送**,不要分五次推。
推送后核对远端 SHA,并确认 `GET /api/health``.deployment.gitCommit` 等于本次 SHA
否则视为未部署。
6. **不要自行提升到 `main`。** 交付到 `staging` 并在 `https://staging.jyotisha.chat` 完成验收后停下,
由人决定是否提升。
---
## 交付物清单
- [ ] 任务 0 复核表(含与审计数字的差异说明)
- [ ] 任务 1 渲染基准脚本 + 改动前读数(或止损说明)
- [ ] 任务 2 五条清理(每条独立验证;2.1 附 build 渲染模式对比)
- [ ] 任务 3 消息列表拆分 + 前后读数 + 反向验证(或止损说明)
- [ ] 任务 4 第一刀 + `Home``useState` 前后数字 + 前后读数(或止损说明)
- [ ] 任务 5 admin 样式隔离 + CSS 体积前后对比(或止损说明)
- [ ] `docs/BUG_HISTORY.md` 建档(编号已核远端)
- [ ] `PROGRESS-frontend-optimization-20260828.md`
- [ ] `BLOCKED.md`(若有止损触发)
- [ ] 第 09 条深色主题待决策项已上交