Files
Jyotisha/PROGRESS-frontend-optimization-20260828.md
T
Jesse_Chen 3198fb6b2b
Independent Staging Quality Gate / validate (push) Successful in 10m14s
Independent Staging Quality Gate / publish (push) Successful in 11m5s
perf(frontend): split chat streaming, load Inter, isolate admin CSS
Settled messages no longer rebuild on every token, Inter is actually
requested, and admin routes drop the 33 KB chat stylesheet. Root
force-dynamic is gone so public shells can prerender without changing
the no-store Cache-Control contract.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 03:39:43 +08:00

9.2 KiB
Raw Blame History

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:10904313 10904313 一致
Home 行数 3,224 3,224 一致
HomeuseState 61 61 一致
HomeuseEffect 19 19 一致
HomeuseMemo / 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 5file/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,约 1322 s;当时全部路由 ƒ Dynamic 基线
/ 首屏 JS / CSS gzip 待实测 JS 16 chunk / 438.5 KBCSS 2 chunk / 33.7 KBglobals 约 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.memoSSR 每次走完整树)。基准测的是拆分后的工作量:已完成列表渲染 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 仍对 //loginprivate, no-store/login 自己读 headers()。会员页数据在客户端拉,静态壳可接受。

既有断言改动(锁的是错误实现,不是“聊天 HTML 不缓存”):
frontend/tests/stale-client-recovery.test.ts 不再要求根布局 force-dynamic,改为禁止根布局再写这行,并锁 login/reports/admin 仍各自声明。反缓存合同仍是 next.config.tsCache-Control

2.2 Inter — 选 (a)

next/font/google Interdisplay: "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/rectificationtests/test_api_async_job_contract.py 锁路径。任务书检索不全。移走会改 tests/** 既有断言。见 BLOCKED.md

2.4 public 资源

删除 5 个无引用 SVG。logo 按 2×(约 64px)重导出并 optimize=True64×62 / 8,584 B(原 33,511 B)。未换成 SVG(三维渲染)。email-otp-loginnext/image alt=""

2.5 合并 effect

一个 effect、三个显式分支。建档 BUG-429。新增测试,未改 chart-library-other-profile 既有断言。

任务 3 · 消息列表拆分 — 交付

ChatTranscript = SettledMessageListmemo+ StreamingMessageEntry(memo)。流式期间历史 props 不含 token 文本。禁止手写 useMemo/useCallbackhandler 走 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 第一刀(账户弹层)— 交付

AccountDialogOverlayopen === false 时 props 稳定(openEpoch 固定为 0),memo 跳过。打开时从 modelRef.current 读最新模型。chartLibraryOpen 下沉到 overlay。

  • Home useState61 → 60
  • Home useEffect19 → 18(含任务 2.5
  • 未碰生时校正外壳
  • DESIGN.md 弹层契约:关闭按钮 aria-label="关闭"、dialog aria-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.tserror.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

/ 现为 ○ StaticHTML 可预渲染。/ 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.memofrontend/src 0 transcript + overlay

既有测试断言改动(仅上述三处)

理由见 2.1 与任务 3。其余 frontend/tests/**tests/** 未改语义。类型夹具 as const / Object.assign 只为 tscBUG-409),不断言行为。

止损

  • 2.3:不删 web/*.html
  • 3/4:未触发 <20% 还原
  • 5:源码隔离后若 error/not-found 仍 import globalsadmin 会拿回 33 KB;已从这两个边界拿掉
  • 后台登录目视:阻塞

第 09 条 · 深色主题(待决策,人拍板)

globals.csscolor-scheme: light0 处 prefers-color-scheme。DESIGN.md 把纸感暖白写成产品身份,但 23,000 字文档没写“永远不做深色”。这是产品决策,不是本轮代码任务。 需要人决定:维持单一浅色,或另开一轮做深色主题。

最终测试数

基线 2196 → 本轮 2205 pass / 0 fail / 0 skipped+9chart-library-session 5、home-streaming-render-split 1、site-style-isolation 2、account-dialog-overlay 1)。

浏览器(next start PORT 3010,无后端):/login 可见压缩 logo 与 Inter 变量 class、Link 头 woff2 preload/membership 站点样式完整;/admin/users 仍 403。未登录 / 因本机 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