Files
Jyotisha/TASK-frontend-optimization-20260828.md
T
Jesse_Chen bf697d71a2 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
2026-08-28 18:38:53 +00:00

306 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 条深色主题待决策项已上交