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

18 KiB
Raw Blame History

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=0fail=0 不得删测试、不得 skip 测试来换绿。
  5. 不得改 CI 配置.gitea/.github/)。
  6. 不得动 frontend/tests/**tests/** 里的既有断言;新增测试可以,改既有断言不行——除非你能证明该断言锁的是错误行为,并在进度文档里单独论证。
  7. 不得在存在未提交修改的工作树上切分支 / stash / reset / 顺带提交用户变更。

让步顺序

功能与测试不回归 > 可被验证的性能收益 > 代码整洁度 > 构建速度。

任何一步做不到时,写进 BLOCKED.md 并停在那一步,不要绕路硬做。本仓库已经有过一次 "配置开了、构建绿了、实际零收益"的教训,宁可少交付也不要交付看起来完成的东西。

已关闭的路(别重跑)

PROGRESS-react-compiler-20260817.mdBLOCKED.md 记录了完整调查: Home 无法被 React Compiler 编译,失败全部来自上游编译器的 Todo:(未实现语法)与 Invariant:(编译器内部断言,即编译器 bug), 本仓库代码没有写错。稳定版 1.0.0 与 experimental 版的全项目编译成功数都是 134,一个没多。 所以:不要再去升编译器版本、不要再去装 babel-plugin-react-compiler 做诊断、不要再开 reactCompiler: true 试。 那份文档给出的下一步就是本任务书的任务 4。


开工前置

AGENTS.md 第 5、6、7 节执行,不可省略:

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-dynamicglobals.css字体localStoragechartLibrary), 命中记录的防复发措施逐条确认是否仍然存在。

任务 0 · 基线复核(必做,不产出代码)

npm installworktree 是新的,没有 node_modules),然后逐项实测并填表:

复核项 审计给的数 你的实测 结论
Home 起止行 page.tsx:10904313
Home 行数 3,224
HomeuseState 61
HomeuseEffect 19
HomeuseMemo / 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:8export 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:92frontend/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.htmlweb/rectification.htmlweb/evidence_packet.html 全仓库检索不到引用,mcp_server.py 也没有挂载静态目录。它们把 API 响应直接 JSON.stringify 打印给用户,主色 #006b6b 与现行设计系统无关,是更早的原型残留。

  • 删除前必须自己再检索一遍(含 deploy/.gitea/Dockerfiledocs/),确认真的没有引用。
  • 若确实还当调试夹具用,移到 scripts/ 下并在文件头写明用途,不要留在 web/ 冒充产品界面。

2.4 清理 public/ 脚手架资源与 logo 体积

  • 删除 vercel.svgwindow.svgfile.svgglobe.svgcreate-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/imagealt=""(装饰性,DESIGN.md 第 81 行有据),保持不变。

2.5 合并两个重叠的 effect

frontend/src/app/page.tsx:1302page.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=0npx 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 的入参里有 isLoadingactiveStreamingText 等会随 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 引用保持稳定。
  • HomeuseState 数量必须实际下降,并在进度文档里给出前后数字。 如果拆完 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.csscolor-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=0npx 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 第一刀 + HomeuseState 前后数字 + 前后读数(或止损说明)
  • 任务 5 admin 样式隔离 + CSS 体积前后对比(或止损说明)
  • docs/BUG_HISTORY.md 建档(编号已核远端)
  • PROGRESS-frontend-optimization-20260828.md
  • BLOCKED.md(若有止损触发)
  • 第 09 条深色主题待决策项已上交