Files
Jyotisha/docs/tasks/PROGRESS-home-first-load-20260930.md
T

14 KiB
Raw Blame History

PROGRESS · 进站等待太久:首页首开与字体加载 · 2026-09-30

任务书:docs/tasks/TASK-home-first-load-20260930.md。产品 2026-09-30「请你调用 subagent 执行」,由 Claude 派 fork 子代理直接执行;验收由 Claude 另做。 分支 codex/home-first-load-20260930(未推送)。基线 origin/staging = 6a74a230(开工实测;任务书写单时是 d87ec44a,中间只多了文档提交)。BUG-1127~1130(开工核对最大号 BUG-1130;另有 BUG-1131 已被别的任务书预留,本单没有新开号)。

提交

提交 内容
80ea7b0b T2 加载屏标题用黑体(BUG-1129)
ab656e0d T1 冷启动账户 / 人物目录 / 会话列表 / 校正入口同一轮(BUG-1127)
2fd1e2b0 T8 HTML 内联脚本早发四个请求(BUG-1127)
b743b16b T7 城市表按需、封存 JSON 只留 6 字段、引导页按需 + 揭幕前预加载(BUG-1130)
ed4f5133 测试存储桩类型修正
8112f62b T3-b 宋体切片搬 public/ + 内容哈希 + immutable(BUG-1128)
31d07378 测量脚本 frontend/scripts/measure-home-first-load.mjs
e2c3791f 引导页改为模块级 React.lazy(lint 规则);暖快照测试按启动流程先加载城市表
分支最后一个提交 记录:BUG 历史、CHANGELOG、DESIGN、BLOCKED、真机清单、本进度、任务板

每项结果

任务 结果 说明
T2 加载屏黑体 做了 .app-loading-content strong → --font-body;合同测试钉住;DESIGN.md 已写。load 前宋体请求 4 → 1,剩下 1 个是侧栏品牌字「Jyotisha」(见偏离 1)
T1 接口并行 做了 账户、人物目录、会话列表(按本地记住的人物猜)同一轮;人物变了或换了账户就丢弃会话列表重取;首登不再作废刚读到的人物目录;校正入口摘要在账户阶段就发。先确认了摘要不依赖会话列表:它是按用户读的独立接口,原来的等待只来自 effect 的 sessionListSettled 条件,该条件保留(只改了请求发出时机)
T8 早发 做了 内联 ES5 脚本放在根 layout 的 <head>,脚本里用 location.pathname !== "/" 直接返回(见偏离 2);四个请求挂在 window.__jyotishaBootReads;只取用一次、30 s 失效、失败回退正常请求、401 照样登出、另一人物或另一账户的会话列表丢弃;路径和参数由 boot-early-reads.ts 共用
T7 首屏瘦身 部分做了 城市表、封存评测 JSON、引导页(及其下的出生地选择器、生时录入、日期选择器)已离开首屏。校正界面、markdown 渲染没做:需要在「打开校正、打开会话、落在 ?c= 会话」几个入口都加揭幕前等待,改动面大,按让步顺序(至少完成第 1、2 项)留给下一单
T3 宋体只下一次 做了(T3-b) T4 不成立,走 T3-b;决策 2 授权推翻的那条断言已按三栏改
T4 JS 不随部署失效 没上(blocked) 第一条证据不成立,见下;写入 BLOCKED.md
T5 准备阶段 JS 预取 没做 可选项;T7 已把引导页挪走,剩下的按需 chunk 大多是揭幕后的次要面板,留给下一单
T6 记录 做了 BUG 历史四条、CHANGELOG、DESIGN、BLOCKED、真机清单 docs/testing/home-first-load-20260930.md、任务书决策 6 补一句、任务板改「待验收」

前后对照

测量:本机 next build + next start(基线在一次性 detached worktree 6a74a230,最终版在本分支),frontend/scripts/measure-home-first-load.mjs,/api/* 统一延迟 600 ms 返回虚构数据,窗口 390×844,每组 3 次。机器走代理且有波动,只看轮数与先后,不代表真机秒数。

模拟慢网(--throttle slow:1.6 Mbps / 150 ms,最稳定的一组):

指标 基线 最终
揭幕时刻 7942 / 7954 / 7956 ms 5617 / 5630 / 5630 ms(−29%)
最后一个首屏 JS 下完 ≈5.43 s ≈4.11 s
首个接口发出 首屏 JS 下完之后 +25~29 ms 首屏 JS 下完之前 ≈3.17 s
从首个接口到揭幕的串行轮数 4 2
load 前的宋体切片请求 4(00/01/03/04) 1(00,侧栏品牌字)

不限速(本机波动较大):基线 3.06~4.36 s(更早一次空闲时测得 2.71~2.74 s),最终 0.92~1.31 s(更早一次空闲时测得 0.90~0.97 s);轮数 4 → 2(有一次记为 3,是某个请求晚发了半轮以上)。

首页首屏 JS(排除 noModule,按预渲染 / 引用的 chunk 计):

文件数 未压缩 brotli gzip
基线 29 1994 KB 516 KB 611 KB
最终 28 1608 KB 431 KB 504 KB

首屏 gzip −17.5%,超出 ±2%,属任务书 T7 验收 3 所述的预期下降。城市表(梅河口市)、封存 JSON(previous_pilot_id)、react-day-picker(rdp-)三个标记都不再出现在首屏 chunk 中,markdown(micromark)仍在。归因构建(productionBrowserSourceMaps)只在一次性 worktree 里做,没有提交。

宋体:本地生产构建里 CSS 的切片地址是 url(/fonts/serif-sc/jyotisha-serif-sc-00.1812be0085.woff2),换部署编号(new00001 → new00002)后地址不变、不带 ?dpl;next start 返回 Cache-Control: public, max-age=31536000, immutable。「staging 连续两次部署地址不变」要部署后由 Claude 核对。

T4 三条证据

  1. 在一次性 worktree 里给 next.config.ts 加 supportsImmutableAssets: true,用 NEXT_DEPLOYMENT_ID=t4aaaa01 做生产构建:.next/static 下只有 build-* / chunks / media,没有 immutable 目录;预渲染 / 的 HTML 里 133 处资源地址仍是 ?dpl=t4aaaa01。不成立。
  2. 未测:第 1 条已不成立,两次构建路径是否相同没有意义。
  3. 未测:同上。

结论:不改配置,写入 BLOCKED.md;Next 源码里 supportsImmutableAssets 的生效依赖 adapter(build/adapter/*),自托管 standalone 没有这一步。

测试数字

项 基线(6a74a230 干净 worktree) 最终
tsc --noEmit — 0 错
npm run lint 0 error,126 warning 0 error,126 warning(与基线同数,本单新文件 0 告警)
npm test(Node 22) 4489 / fail 24 / cancelled 0 4513 / fail 24 / cancelled 0
失败名单 24 条 与基线逐条一致(全是要 Docker 的数据库、部署、备份套件)
测试名 — 新增 24 条,没有消失的
next build / Static / Static
快速门 Python 子段 1001 passed / 1 skipped 1002 passed / 1 skipped(+1 = test_slice_names_carry_their_content_hash…)
快速门 npm 子段(系统 Node 20) 4489 / fail 66 4513 / fail 66,名单逐条一致(环境:Node 20)
tests/test_repo_privacy_markers.py — 通过(快速门内)

改动的既有断言与夹具(每处都写了原值 / 新值 / 原因):

  • serif-headings-contract.test.ts:切片 url 从 ./jyotisha-serif-sc-NN.woff2 改为 /fonts/serif-sc/…<哈希>.woff2(决策 2 授权推翻);新增加载屏黑体、长期缓存两条。
  • font-stack-loadable-contract.test.ts:绝对地址按 public/ 解析,「url() 文件必须存在」不变。
  • tests/test_serif_font_slices.py:切片目录改为 public;新增文件名哈希与内容一致、public 旁有 OFL。
  • chart-page-view.test.tsx、session-list-provider.test.ts:会话、账户请求的源码断言改为共用常量的写法,并断言常量值不变。
  • home-warm-snapshot.test.ts:换账户分支的源码断言改为 boundAccount 写法;用例前先加载城市表(夹具是只有编码的老资料)。
  • home-bootstrap-run.test.ts:夹具 io 补 preloadOnboardingShell / startEntrySummaryRead 空操作,断言不动。
  • rectification-confirmation-gate.test.ts:新增「6 个字段逐字段等于封存 JSON」。

偏离任务书之处

  1. T2 验收「load 前 0 个宋体请求」未完全达到:剩 1 个,是侧栏品牌字「Jyotisha」(.brand-row,按 serif-headings 方案属于标题,静态 HTML 里可见),只用 00 号切片(38 KB,拉丁字母)。它不在决策 1「加载屏改黑体」的范围内,没有擅自改;切片现在长期缓存,每台设备只下一次。是否也改黑体,由产品决定。
  2. T8 内联脚本放在根 layout,用路径判断只在 / 生效,而不是「只在首页 HTML 里」。首页 (app)/page.tsx 是客户端组件,在里面渲染 <script> 会进 <body>、执行更晚,还有水合风险;根 layout 已有同类的 BUG-936 兜底脚本。其他路径上脚本第一行就返回,不发请求(有测试覆盖 /chart /reports /login /people /admin)。
  3. T1 里账户切换时,早发 / 预取的会话列表一律丢弃(按红线 8),即使人物同为本人;代价是换账户那次多一轮,很少见。
  4. 城市表的「揭幕前加载」挂在人物目录加载里(目录总包含本人资料),而不是 loadSessionList:upsertSelfChart 本身就要判断资料是否完整,必须在它之前加载。加载失败时,老资料会像没有表时一样被当成「地点未完整」(原来不会发生);网络断开才会遇到,没有另做兜底。
  5. 引导页若在揭幕后才需要(例如揭幕后资料被重置),走普通按需加载,空白一瞬(fallback 为空,不是 spinner)。

环境缺口

  • 没有登录态与真机:浏览器级验收见 docs/testing/home-first-load-20260930.md(七条)。本地测量用虚构账户接管接口,真机网络下的秒数以产品掐表为准。
  • 没有 Docker:DB / 部署套件 24 条失败,与基线逐条一致。
  • worktree 里没有 .venv:pre_work_check.py 退出码 0,但外部引擎诊断那一项报「找不到 .venv/bin/python」;快速门借用本机 .worktrees/rectification-holdout-expansion-20260929/.venv(临时软链接,已删)。
  • 「staging 连续两次部署宋体地址不变」「staging 上的揭幕时间」要部署后由 Claude 复核。

Claude 独立验收(2026-10-01)

项 结果
tsc --noEmit / npm run lint 0 错 / 0 error(126 warning,与基线相同)
npm test 4513 条,失败 24 条,与 Claude 在 d87ec44a 上跑的失败清单逐条一致(均为需要 Docker 的套件);按测试名比对没有消失的,新增 24 条
next build 通过,/ 仍是 ○ Static
Python tests/test_serif_font_slices.py、tests/test_repo_privacy_markers.py 通过
字体 本地生产服务:CSS 里的宋体地址为 /fonts/serif-sc/<名>.<哈希>.woff2,不带 ?dpl;响应头 Cache-Control: public, max-age=31536000, immutable、Content-Type: font/woff2
T8 早发 Claude 自己的 CDP 脚本(接口统一 600 ms):account、chart-profiles、models、sessions 在导航后约 50 ms 同时发出,早于首屏 JS;揭幕前只剩 2 轮(第二轮是 entry-summary / consult status)
未登录 所有接口返回 401 时,约 0.44 s 跳到 /login
代码审阅 早发脚本只在 / 生效,每个结果只用一次、30 s 后作废、网络失败回退到正常请求;猜错的人物、换账号时丢弃预取的会话列表;确认门槛把研究 JSON 换成用到的 6 个值,并有测试与原 JSON 比对。Caddy 原样转发 Next 的缓存头,镜像会复制 public/,线上能生效

遗留:T2 侧栏品牌字「Jyotisha」在 load 前仍会下载宋体切片 00(38 KB,不在决策 1 的范围内);T7 校正界面与 markdown 仍在首屏;T4 已记入 BLOCKED;T5 未做。部署后要核对:连续两次部署,字体地址不变;在 staging 线上复测。

修复单 · 早发请求被样式表挡住(2026-10-01)

任务书 docs/tasks/TASK-home-first-load-fix-20261001.md,分支 codex/home-first-load-fix-20261001,基线 origin/staging 6e2f0af5(代码同已部署的 e600cff5)。

  • 做法:采用首选方案,<script async src={bootEarlyReadsScriptSrc()} />,src 是原 ES5 脚本的 base64 data URL。构建后的 / HTML 里,它排在 3 个样式表之后,但带 async="",data URL 没被改写,所以没有走 public/ 静态脚本的备选方案。主题脚本和首屏兜底脚本没动。
  • 测量(本地 next start,接口 600 ms,--css-delay 1500,每组 3 次):
第一个样式表放行 早发请求发出 早发早于样式表 揭幕
基线 e600cff5 1543 / 1528 / 1531 ms 1559 / 1558 / 1545 ms 否 2400 / 2392 / 2363 ms
修复版 1545 / 1533 / 1527 ms 45 / 32 / 34 ms 是 2392 / 2387 / 2369 ms
修复版,iPhone UA + 390×844 1521 / 1538 ms 21 / 42 ms 是 2352 / 2403 ms

本地的揭幕时间没有变:样式表拖住了首屏 JS,而本地接口 600 ms 比 1.5 s 的 CSS 延迟短,早发提前的时间被 JS 等待盖住了。线上 CSS 与 JS 都走真实网络,早发提前约 2.4~3.1 s,可以和 JS 下载并行,要部署后复测。

  • 取用:两种 UA 下,揭幕后 window.__jyotishaBootReads 只剩 startedAt,四个早发结果都已被取用。
  • 测试:tsc 0 错;lint 0 error,126 warning(同基线);npm test 4513 / fail 24 / cancelled 0,失败名单与基线逐条一致,测试名无增减;next build 通过,/ Static。改了一条断言(layout 装载方式,已写三栏),并新增「data URL 解码后与 ES5 脚本逐字节相同」。
  • 环境缺口:无头 Chrome 只换了 UA,引擎不是 WebKit;真实 iOS Safari 是否执行 async data: 脚本,留在真机清单第 8 步。

Claude 线上复测(staging ca1353eb,2026-10-01)

同一 CDP 脚本打开 staging 线上 /,/api/* 统一延迟 600 ms,每组 3 次(本机走代理,只看先后与轮数):

版本 /api/account 发出 揭幕
本单开始前(d87ec44a) 3050–3310 ms(首屏 JS 全部下完后) 5586–6002 ms
第一轮(e600cff5,早发被样式表挡住) 2357–3104 ms 3844–4240 ms
修复单(ca1353eb) 1073–1184 ms(HTML 一到就发,早于 DOMContentLoaded,也早于第一个首屏 JS 完成) 3456–3620 ms

本单开始前到修复单,揭幕约快 2.1–2.4 s(约 −40%)。部署检查:/login 200,未登录 /api/account 401,health deployment.gitCommit = ca1353eb。修复单验收通过。真 iOS Safari 执行 async data: 脚本的情况留在真机清单第 8 步。