docs(research): code health audit 2026-10-28 (issue #118)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DbBXoz1Qk3y9buDv1mkmTu
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7444964859
commit
a31b6dda39
@@ -0,0 +1,409 @@
|
||||
# 代码体检 2026-10-28(issue #118)
|
||||
|
||||
基线 origin/staging `0f0fea325`。四路只读审计(前端加载与屎山、后端与推理速度),Claude 抽查了排在前面的结论。体检结论摘要与拆单清单见 Gitea issue #118 的评论;本文件是两份原始报告的合并稿,供拆出的小单引用数字和测法。
|
||||
|
||||
口径:「实测」= 审计机本地计时;「估」= 推算或取自 10-04~10-07 的进度记录。审计机没有模型 key、没有 PyJHora 与 VedAstro SDK,涉及这些的生产耗时都是估计。出生资料只用公开名人和一张虚构盘。
|
||||
|
||||
---
|
||||
|
||||
# 代码体检(后端 + 推理速度)· 2026-10-28
|
||||
|
||||
基线 `origin/staging` 0f0fea325(audit-20261028 工作树,只读,`git status` 干净)。
|
||||
|
||||
本机条件:
|
||||
- Python 3.13,swisseph 走 `~/.local`。
|
||||
- 没有 PyJHora、没有 VedAstro SDK。VedAstro 网络关闭。
|
||||
- 没有模型 key。
|
||||
|
||||
标注口径:
|
||||
- 「实测」= 本机计时。
|
||||
- 「估」= 推算,或取自 10-04 ~ 10-07 的进度记录。
|
||||
- 出生资料只用两组:公开名人(Einstein、Helen Keller)和一张虚构盘。
|
||||
|
||||
## 结论先行
|
||||
|
||||
1. **咨询最慢的一段仍是答题步推理。** 「推理 token 1 万已修」实际上没生效:开关 `CONSULT_ANSWER_REASONING_EFFORT` 默认不设,compose 里也没有,A/B 一直卡在 BLOCKED。
|
||||
2. **后端可以一张单拿下的最大块是长报告冷生成,实测 15–23 s。**
|
||||
- 已有的提速开关 `JYOTISH_REPORT_RENDER_FAST` 输出逐字节相同,本机实测快 27%,却默认关着。
|
||||
- 另有约 9 s(profile 口径)花在整包 deepcopy 和 5 次重复哈希上。
|
||||
3. **10-21 PyJHora 进了生产(#93)以后,10-15「装不上就不起子进程」的省法在生产上不再生效。** 报告和非 skip 排盘每次都会起 PyJHora 子进程(估)。
|
||||
4. **咨询每次调 `consultation_workflow`,只要 VedAstro 不命中,就起 3 个 Python 子进程。**
|
||||
- 本机实测:0.55 s 里有 0.40 s 花在这上面。
|
||||
- 生产上最多等 1.5 s。
|
||||
- 失败结果只缓存 300 s。
|
||||
5. 两条链路都还有「第 0 步模型调用」的白耗:
|
||||
- 校正:强制先调一次 read-case。
|
||||
- 咨询:第 0 步开着思考,分类又串行挡在前面。
|
||||
6. 屎山方面:
|
||||
- 226 个顶层脚本(约 3.4 万行)从运行入口不可达。
|
||||
- 20 个模块按 2~3 个名字各加载一份。
|
||||
- Vimshottari 仍有两套起算口径,差 1 天。
|
||||
|
||||
## 问题清单(按影响从大到小)
|
||||
|
||||
| # | 位置 | 问题 | 量化影响 | 建议改法(一张小单) | 验收标准 | 风险 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | `frontend/src/mastra/consultation-tools.ts`:`consultAnswerReasoningEffort` / `answerEffortOptions`(183–207 行);`BLOCKED.md` 第 23 行 | 答题步推理强度开关不设就返回 `undefined`。compose 里没设,A/B(TASK-answer-effort-ab)因为没有 key 一直没跑。10-04 量到的「答题步推理 1 万 token」实际上还在 | 估:答题步推理 token 中位 9.5–11.9k,约 45–60 s(10-04 实测记录)。这是咨询最慢的一段 | 在有 key 的 staging 跑完 answer-effort-ab 的 T3。产品定档后写进 compose(或 `.env`),代码不改 | `/admin/usage` 分段列里,第 1 步推理 token 中位数和写正文用时都下降;回答质量盲评不劣于基线 | 解读深度下降;部分供应商不认这个参数 |
|
||||
| 2 | `scripts/report_perf_flags.py`:`RENDER_FAST_DEFAULT = False` | 渲染快捷路径输出逐字节相同,但因为没达到当时定的 35% 门槛就默认关着 | 实测(虚构盘,full_data zh+en,缓存关):关 15.75 / 15.27 s,开 11.20 / 11.64 s,**快 27%**。两边整份响应 sha256 相同(`59c793e0b28c`,5,262,387 B)。HTTP 冷跑 23.3 s / 6.1 MB,命中 0.08 s | 把默认值改成 True,或在 compose 里设 `JYOTISH_REPORT_RENDER_FAST=1`。保留旧路径作回退 | 用 `scripts/research/report_reference_perf_ab.py`(屏蔽 / 不屏蔽 PyJHora 各跑一遍):四张盘五字段 sha 相同;端到端 −25% 以上 | 低。快捷路径已有逐字节对照测试 |
|
||||
| 3 | `scripts/pl9_full_data_export.py:read_full_data_domain`(2475 行);`scripts/calculation_profile_contract.py:hash_bound_result`(567 行)/ `_drop_key_recursive`(463 行) | 长报告里有两处重复劳动。① 一份报告调 10 次 `read_full_data_domain`,每次都把整包 `worksheets` deepcopy 一遍。② 溯源哈希算 5 次,每次都递归删键再哈希 | 实测(cProfile 口径,总 40.8 s):deepcopy 合计 8.9 s,其中 `read_full_data_domain` 5.3 s;`hash_bound_result` 5 次共 4.3 s,`_drop_key_recursive` 调了 407 万次。按无 profile 折算约 4–6 s / 份(估) | ① 同一个包只深拷一次,各领域共用只读视图;或者只拷该领域要的 worksheet 子树。② 同一个结果对象的绑定哈希按 `id` 记一次,或在 5 处调用的上游只算一次 | 冷生成再降 25% 以上;markdown、json 两版与改前逐字节相同;`result_binding` 哈希值不变 | ① 下游若原地改了拷贝,会串改共享数据。要先 grep 写入点,或改用 MappingProxy 防写 |
|
||||
| 4 | `scripts/report_render/annual_method_reference.py:_invoke_worker`(169 行);`scripts/annual_pyjhora_replay.py`(202 行子进程);`scripts/tajika.py`(1517 / 1579 行);`requirements.txt:7 PyJHora==4.8.7` | 10-15 的 BUG-1333/1334 靠「父进程导入不了 jhora 就不起子进程」省时间。10-21 #93 把 PyJHora 装进了生产,这个省法在生产上不再生效。现在每份报告、每次非 skip 的 `/api/chart` 都会起:年度回放子进程(超时 30 s)、Tajika 子进程、年度方法 worker(每次新开 `python -m`)。worker 起之前也不查 jhora 在不在 | 实测(本机没装 jhora,子进程秒退):一份报告 5 次 `subprocess.run`,等待 5.3 s,其中 worker 4.8 s。估:生产上每个子进程要重新 import jhora 和 swisseph,每个 1–3 s,单份报告多花 5–10 s | 起一个常驻 PyJHora 子进程,或进程内单例:一次 import,多次调用,按请求哈希缓存结果。另外 worker 也要先查 `find_spec`。放在新模块,主文件不增长 | 屏蔽 / 不屏蔽 PyJHora 两种口径下输出逐字节相同;一份报告的 `subprocess.run` 次数从 5 降到 0 或 1;冷生成时间下降 | 当初隔离进程是为了防 PyJHora 全局状态(岁差模式)污染主进程。常驻进程每次调用前都要显式重设岁差 |
|
||||
| 5 | `scripts/vedastro_foreground.py`:`ForegroundVedastroSession`;`scripts/vedastro_service_adapter.py:_run_official_full_snapshot_case`(2549 行,依次起 capability runner、python bridge、capability catalog 三个子进程) | 咨询每次调 `/api/consultation_workflow`,只要快照不命中,就新开 3 个 Python 解释器。只有 `official_verified` 才写进前台快照缓存(`is_cacheable_gateway`);失败结果只在 adapter 层留 300 s。免费层限流时,几乎每轮每个领域都要重新等 | 实测(同一张盘换分钟,5 次中位):join 1.5 s 时 0.546 s,join 0 时 0.143 s,**VedAstro 占 0.40 s / 次**,3 个子进程。估:生产装了 SDK,又要走外网,每次未命中封顶 1.5 s;一问两个领域就是 3 s | 前台快照加负缓存:失败或限流按盘和日期记一整天。capability catalog(只用于覆盖率展示)挪到后台。可选:把三段子进程合成一个常驻 worker | 同一张盘当天第二次调用,`vedastro_gateway` 字段与改前一致,耗时 ≤0.15 s;成功路径输出逐字节不变 | 负缓存期内 VedAstro 恢复了也看不到。TTL 要和产品定 |
|
||||
| 6 | `frontend/src/lib/rectification-agentic/v9/agent-run-attempt.ts:259` 的 `prepareStep`(stepNumber 0 只开放 `rectification-read-case`) | 校正每轮固定 5 次串行模型调用:分类、read-case、记证据、设焦点、写正文,都开思考 8192。其中第 0 步只是为了调 read-case,结果是确定的 | 估:每轮省 1 次带思考的模型调用,约 3–8 s(子代理按记录估)。09-26 记为 D6「另议」,至今没做 | 服务端先执行 read-case,把结果作为合成的工具调用和结果放进 bootstrap,模型从第 1 步开始 | `stepCount` 少 1;轮次中位耗时下降;`case.loaded` 回执和 Skill 收据各只写一次;read-case 投影逐字节不变 | 要改 Skill「第一步调用」的原文;涉及 BUG-282/937 的步骤约束 |
|
||||
| 7 | 咨询:`consultationNatalPrepareStep` 与 `consultationGenerationSettings`(`thinking: "enabled"`);`route.ts` 中 `classifyConsultationTurn`(约 721 行) | 第 0 步开着思考,但只用来发一个工具调用。分类(超时 ≤3 s)又排在扣点和写入问题之后串行等 | 估:第 0 步 ≤5 s(10-04 记录);分类 ≤3 s | (a) 第 0 步关思考,或扣点后立刻按已选主题预开算(复用时要比对领域参数)。(b) 分类和 prepare / append 同时开始 | 分段列里第 0 步耗时下降;`prep.classify` 不再在首字关键路径上 | (a) 产品曾搁置过;(b) 判为寒暄时要撤销已经开始的工作 |
|
||||
| 8 | `scripts/consult_full_data_cache.py`:`get_or_build` / `_read_cached`;`pl9_full_data_export.read_full_data_domain` | 咨询卡缓存命中时也不便宜:每个领域每轮都要读 5.5 MB JSON,再整包 deepcopy。缓存键含服务器当天日期,所以每天首问冷算。缓存文件权限是 0644,含出生资料,没跟随 0600 规则 | 实测:命中路径 `attach_consult_full_data` 0.425 s(profile)/ 约 0.2 s(正常),其中 JSON 解析 0.06 s、deepcopy 0.2 s;HTTP 冷首问 5.4 s。估:跨日首问 4.4–7.3 s(10-07 记录) | ① 进程内按 key 做小 LRU 缓存已解析的包(2–4 份),读视图时不再深拷。② 进入咨询页或 `runTurn` 开头不等待地发一次 `consult_card_warm_only`。③ 写缓存改用 `write_private_json` | 命中路径 ≤0.05 s / 领域;跨日首问 `consult_card_facts.timing.hit=true`;`consult_card_facts` 逐字节不变;新文件权限 0600 | LRU 占内存,5 MB × N,要在 4 GB 主机上核算;预热占重计算名额 |
|
||||
| 9 | `scripts/jyotish_api_server.py:27/10885`,`ThreadingHTTPServer` 单进程 | 重计算(v5 score、block_scan、报告)都是纯 Python CPU,受 GIL 限制。2 vCPU 的第二核在 Python 侧基本闲置,`JYOTISH_HEAVY_COMPUTE_CONCURRENCY=2` 等于两人抢一个核 | 实测(子代理):121 候选打分单跑 1.19 s;两路并发各 2.35–2.6 s(约 2.2 倍) | 新模块里给 v5 score / block_scan / 报告开进程池,`max_workers` = 重计算并发数,主文件只做薄注册 | 两路并发各 ≤1.3 倍单跑;规范化 JSON 与改前一致 | 内存(每个 worker 带一份引擎);worker 必须固定 `PYTHONHASHSEED`(ERR-111) |
|
||||
| 10 | 校正交付期:`jyotish_api_server.py:_attach_vedastro_minute_sensitive_to_score`(约 428–515 行);前端 `score-persist.ts:persistableReceipt`(约 330 行) | 交付期 score 内部最多等 3 s 快照,随后前端又串行调 `/v5/vedastro-validate`(超时 12 s)。Python 侧区间扫描依次发,每次 20 s,前端放弃后它还在跑,并占着重计算名额。状态不是 passed 就每次重跑 | 估:交付期一轮最坏约 15 s;失败时每轮重复 | validate 结果按(证据指纹, 候选对)做负缓存;区间扫描按分钟 / 领域 / 日期缓存;Python 侧超时收到不超过前端的超时 | 同一状态第二次调用耗时接近 0;VedAstro 成功时回执逐字节不变 | 负缓存期内看不到恢复 |
|
||||
| 11 | 咨询窗口问法:`consultation-tools.ts:executeWindowConsultation` | `/api/declared_window_chart` 和 `/api/rectification/v5/range_reading` 串行,但后者不依赖前者的结果 | 估:0.3–1 s / 轮 | 改成 `Promise.all` | 工具耗时 ≈ 两者中较长的一个;输出不变 | 低 |
|
||||
| 12 | 开关默认值:`frontend/src/lib/consult/prep-flags.ts`(`CONSULT_PREP_PARALLEL` 关,`MODEL_VERSION_CACHE_TTL_MS` 0,`CONSULT_DOMAIN_CONCURRENCY` 1);`api_heavy_compute_gate.py`(`JYOTISH_GATE_CHART_OVERVIEW` 关) | compose 里都没设,`.env` 按要求没读。关着时:首帧前多等一次审核(接阿里云时最多 1.5 s);两领域问题串行(每领域 0.75–1.66 s) | 估:每轮 0.1–3 s,取决于审核供应商和领域数 | 先在服务器核对 `.env` 的 `MODERATION_PROVIDER`。是 aliyun 就开 `CONSULT_PREP_PARALLEL=on`。领域并发等第 9 条做完再开 | `consultation-prep-v1` 日志里首帧前耗时下降 | 领域并发会让一个用户占满 2 个重计算名额 |
|
||||
| 13 | 校正:`dasha_transition_proximity.py:164-181`、`rectification/event_probes.py:1812` | 大运起点表每个候选重复算 3 遍(vim 两遍、那罗延一遍,引导窗口再一遍) | 实测(子代理):`_vim_start_dates` 479 次、`_narayana_start_dates` 542 次(121 候选);约占打分 20%,估 −0.15–0.25 s / 121 候选 | 静态上下文顶层每个候选建一张全量表,两处按年份过滤取用 | 两函数调用次数各 ≤121;11 个场景 sha256 不变 | 要按研究协议重新冻结(ERR-110) |
|
||||
| 14 | `scripts/jyotish_api_server.py:_load_local_module`(2533 行,用 `_jyotish_local_<name>` 另起一份模块),再加上 `scripts.X` / `X` 两种 import 写法 | 同一个文件按 2~3 个名字各加载一份。各份的模块级缓存(lru_cache、memo 字典、`_jhora_spec_cache` 等)互不共享,`isinstance` 跨份也会失败 | 实测:一次咨询 + 一次排盘后,105 个脚本模块里有 20 个重复加载(共 2.8 万行),`jyotish_engine` 加载了 3 份。内存只多几 MB,主要是缓存命中被拆散 | `_load_local_module` 先查 `sys.modules` 里的 `scripts.<name>` / `<name>`,有就复用;统一 `sys.path` 口径。只改加载器,不动调用点 | 重复加载数 20 → 0;增长护栏不涨;全量 pytest 与基线失败清单一致 | 有个别测试依赖「独立副本」去打补丁,要跑全量 |
|
||||
| 15 | `scripts/dasha_analyzer.py:build_dasha_timeline`(85 行,`strptime("%Y-%m-%d")`)与 `scripts/jyotish_engine.py:cmd_dasha`(约 12290 行,用真实出生时刻) | Vimshottari 还剩两套起算口径。`/api/chart`(经 `domain_calculation_service.compute_vimshottari_timeline`)和校正探针从**出生当天 0 点**起算;报告和完整解读从**真实出生时刻**起算。`dasha_analyzer` 里另有一份年数表字面量(白名单放行) | 估:两边 MD / AD 边界最多差 1 天。出生时刻越晚,日期串落到不同日的概率越高(约等于出生小时 / 24)。星盘页和报告可能对不上一天 | `build_dasha_timeline` 增加 datetime 入参,`domain_calculation_service` 传真实出生时刻;年数表改读 `dasha_common` | 用 golden 盘对比 `/api/chart` 与 `cmd_dasha` 的 MD / AD 边界,逐项相等;校正 11 个场景要重新冻结(会变) | 改变用户可见日期,属于数值变更,要按「引擎改值跑全量」执行 |
|
||||
| 16 | `scripts/` 根目录 | 从运行入口(api_server、engine、mcp_server、前端 / deploy 引用的脚本、quality_gate、pre_work_check)静态不可达的脚本有 226 个,约 33.9k 行;其中 208 个只被 tests 引用。`dasha_calculator.py` 已无任何运行时引用。python-hygiene 只加了「数量只降不升」的护栏 | 实测:459 个顶层脚本里 226 个不可达(字符串匹配宽松,这是下限) | 先把台账 / closure / matrix / inventory 类(约 84 个文件名命中)连同只测它们的测试,一起挪到 `tools/ledger/` 或删除;一单只处理一个前缀 | 快速门与全量 pytest 失败清单不变;`scripts/` 根目录 `.py` 数下降 | 有些被 workflow 或人工命令调用,要先核对 `.gitea` 和 docs |
|
||||
|
||||
### 后端超长文件前 10(行数,实测)
|
||||
|
||||
| 文件 | 行数 |
|
||||
| --- | --- |
|
||||
| `scripts/jyotish_engine.py` | 18,679 |
|
||||
| `scripts/classical_astrology.py` | 13,065 |
|
||||
| `scripts/jyotish_api_server.py` | 10,949(冻结) |
|
||||
| `mcp_server.py` | 5,220 |
|
||||
| `frontend/src/lib/personal-report-generation.ts` | 3,740 |
|
||||
| `frontend/src/lib/rectification-agentic/v9/method-followup.ts` | 3,574 |
|
||||
| `scripts/pl9_reader_export.py` | 3,390 |
|
||||
| `scripts/muhurta.py` | 3,373 |
|
||||
| `scripts/vedastro_service_adapter.py` | 3,214 |
|
||||
| `scripts/report_render/parity_chapters.py` | 2,767 |
|
||||
|
||||
其后依次是 `answer-choice.ts` 2,641、`rectification-v9-tools.ts` 2,638、`pl9_full_data_export.py` 2,498。
|
||||
|
||||
### 超长函数前 10(实测)
|
||||
|
||||
| 函数 | 位置 | 行数 |
|
||||
| --- | --- | --- |
|
||||
| `render_pl9_markdown` | `jyotish_engine.py:3665` | 6,506 |
|
||||
| `render_pl9_parity_markdown` | `pl9_reader_export.py:637` | 2,754 |
|
||||
| `cmd_full_reading` | `jyotish_engine.py:15368` | 1,653 |
|
||||
| `build_pl9_style_export_packet` | `jyotish_engine.py:17023` | 934 |
|
||||
| `_render_pl9_ai_density_raw_data_markdown_en` | `pl9_full_data_export.py:1431` | 850 |
|
||||
| `_auxiliary_dasha_section` | `jyotish_engine.py:7608` | 761 |
|
||||
| `_sanitize_pl9_ai_density_markdown` | `pl9_full_data_export.py:97` | 451 |
|
||||
| `main` | `jyotish_engine.py:18239` | 437 |
|
||||
| `_derive_event_judgement` | `mcp_server.py:2317` | 423 |
|
||||
| `_release_adjudication` | `classical_astrology.py:801` | 419 |
|
||||
|
||||
另外:`execute_consultation_workflow` 400 行(`api_server:2131`);`_sanitize_pl9_ai_density_markdown` 一个函数在 profile 里占 8.6 s,主要是 7.1 s 的 `re.sub`,第 2 条的开关打开后会走快捷路径。
|
||||
|
||||
## 10-13 体检项核实结果(看代码与实测,不看状态板)
|
||||
|
||||
| 项 | 结论 | 证据 |
|
||||
| --- | --- | --- |
|
||||
| 报告 worker `defaultSleep` 漏摘监听 | **已修** | `personal-report-worker-core.ts:197`,到点就 `removeEventListener` |
|
||||
| `guided_collect_windows` 按候选对重复算 | **已修** | `event_probes.py:1871` 起按(轨道, 序号)去重;`_context_time` 从 278 万次降到 2.4 万次(子代理实测) |
|
||||
| 功能吉凶 / Narayana 每候选×事件×日重复算 | **已修(循环内)** | `derive_functional_benefic_malefic` 从 6,519 次降到 121 次;`_active_narayana` 改走子运表;探针路径还剩 348 次 `get_current_narayana_dasha`,约 0.03 s |
|
||||
| 打分响应 2 MB 含前端不读字段 | **已修** | 矩阵只在 `include_internal` 时返回,实测 0.84 MB。另外特征快照里 `kp_cusps` 约 298 KB 没人读,属卫生问题 |
|
||||
| 长报告 18.5 s 不缓存 | **部分** | full_data markdown 按 `GITHUB_SHA` 缓存,命中实测 0.08 s;但冷生成仍 15–23 s,每次部署全部失效;提速开关默认关(第 2、3 条) |
|
||||
| `/api/chart` 非 skip 每次起 2 个子进程、不受闸门管 | **部分,生产上已回退** | 前端请求全部带 skip(有合同测试);闸门开关 `JYOTISH_GATE_CHART_OVERVIEW` 默认关;「装不上 PyJHora 就不起子进程」在 #93 装进生产后失效(第 4 条) |
|
||||
| chart 缓存清理扫错目录 | **已修** | `api_scratch_retention.py` 登记了 `api_chart_cache` 等 9 个目录;但 `consult_full_data_cache` / `report_reference_cache` 文件仍是 0644(第 8 条) |
|
||||
| `render_pl9_markdown` 单函数 6917 行 | **部分** | 第一阶段搬出 34 个嵌套函数,现在 6,506 行、114 个嵌套函数,第二阶段没做 |
|
||||
| api_server 伪造 handler(#70) | **已修(scripts 内)** | `scripts/` 和 `mcp_server.py` 里已没有 `JyotishAPIHandler.__new__`,改用 `JyotishComputeService` 和 `offline_compute_mixins.py`;tests 里还有 10 处 `__new__` / `object.__new__` |
|
||||
| dasha 两套实现(#69) | **部分** | 年数表已收进 `dasha_common`(`dasha_analyzer` 和 `tajika` 在白名单里);但起算口径仍是两套(第 15 条);`dasha_calculator.py` 无运行时调用方,`dasha_calculator_enhanced` 仍被 api_server 用作「五级大运」 |
|
||||
| 138 个只被测试用的台账脚本 | **未修** | 现在静态不可达 226 个(33.9k 行),208 个只被测试引用;只加了数量护栏(第 16 条) |
|
||||
| 旧校正链约 2.6k 行 | **已修** | 旧 agentic 链(`rectification-tools.ts`、`session.ts`、`/api/birth-rectification` 等,合计 3,191 行)已删;`birth-time-journey*` / `birth-time-scoring*` 约 4.8k 行仍有首页引导在用,不是死代码 |
|
||||
|
||||
## 环境缺口
|
||||
|
||||
- 没有模型 key:第 1、6、7 条的秒数取自 10-04 ~ 10-07 的记录,要用 staging `/admin/usage` 分段核实。
|
||||
- 没有 PyJHora 和 VedAstro SDK:第 4、5、10 条的生产耗时是估计,本机只量到「子进程秒退」时的下限。
|
||||
- `.env` 没读:第 1、12 条的开关在生产上是否另行设置,需要产品在服务器核对。
|
||||
|
||||
---
|
||||
|
||||
# 前端体检(加载慢 + 屎山)· 2026-10-28
|
||||
|
||||
基线:`.worktrees/audit-20261028` = origin/staging `0f0fea325`。只读,没有改仓库。构建用 Node 22 跑 `next build`,再跑 `next experimental-analyze -o`,按模块归因。`/` 仍是 `○ Static`。
|
||||
|
||||
**口径。** 首屏 gzip 的量法和 DESIGN.md 一样:取 `.next/server/app/index.html` 引用的 39 个 js/css 块,每个 `gzip -9` 后求和。
|
||||
- 实测 **702,985 B**。#111 接受的基线是 694,941,现在 +1.16%,还在 ±2% 内。
|
||||
- 其中 39,392 B 是 `polyfill-nomodule`(`<script noModule>`),现代浏览器不下载。所以真实首屏约 **663.6 KB**。两次基线都含这一块,可比。
|
||||
- 各路由首载 JS 的 gzip-9(实测,`route-bundle-stats.json` 的块求和):`/chart` 579 KB,`/` 583 KB,`/people` 543 KB,`/synastry` 486 KB,`/reports` 429 KB,`/ephemeris` 433 KB。
|
||||
|
||||
标记:「实测」= 本次构建或代码里数出来的;「估」= 按比例推算,或要浏览器才能测。
|
||||
|
||||
---
|
||||
|
||||
## 一、按影响从大到小
|
||||
|
||||
### F1 | 星盘页、档案页首屏带上整张城市表和出生资料编辑器(BUG-1130 在次级页复发)
|
||||
- **位置**
|
||||
- `src/components/chart-page/chart-page-view.tsx`,第 24 行静态 `import { ChartBirthEditor }`。它只在 `editingBirth` 为真时渲染(第 303 行)。
|
||||
- `src/components/people/people-page.tsx`,第 8 行静态 `ChartProfileForm`。
|
||||
- `src/lib/people-archive-view.ts:peopleCityLabel`,经 `china-birth-place` 静态 import `data/china-locations.json`。
|
||||
- **问题**:BUG-1130 只把城市表挪出了 `/`。`/chart` 首载仍静态带着「改资料」弹窗的整条链:ChartBirthEditor → ChartProfileForm → profile-fields → birth-place-picker → china-birth-place → china-locations.json,外加 world-countries、react-day-picker、select、radio/toggle-group。这个弹窗要点铅笔才打开。`/people` 列表只为算「城市」一列就整表 import,新增人物的表单也是静态的。
|
||||
- **量化**:`/chart` 首载里只由编辑器引入的块(实测,块 gzip-9)。四块合计 **108.3 KB**,占 `/chart` 首载 JS 的 19%。`/people` 同样带 62.7 + 14.3 + 18.9 + 12.5 KB。
|
||||
| 块 | gzip-9 |
|
||||
| --- | ---: |
|
||||
| `2o97qgvjfxacu.js` 城市表 | 62,653 B |
|
||||
| `3kvn2v-g_kh-c.js` 选地点 + 国家表 | 14,310 B |
|
||||
| `0glp7drjfmf_j.js` react-day-picker + date-fns | 18,865 B |
|
||||
| `2mlkn9k62l__6.js` select | 12,516 B |
|
||||
|
||||
可达性脚本核对过:从 `/chart` 入口去掉 `chart-birth-editor.tsx` 后,这 27 个文件(源码 336 KB)全部不可达。
|
||||
- **建议改法(一张小单)**
|
||||
1. `ChartBirthEditor`、`people-page` 的 `ChartProfileForm` 改成 `next/dynamic`。点铅笔或「新增」时再加载,并在 hover/focus 时预取。
|
||||
2. `peopleCityLabel` 改走 `china-locations-client.ensureChinaLocations()` 和 `loadedChinaCountry()`,与首页同法。老档案(只有省市代码、没有坐标)在揭幕前 await,保持 BUG-1130 的口径。
|
||||
- **验收标准**
|
||||
- `/chart` 首载 JS gzip-9 从 579 KB 降到 ≤ 475 KB,`/people` 降到 ≤ 445 KB。`route-bundle-stats.json` 里这两个路由不再出现城市表块。
|
||||
- 点铅笔照常打开弹窗,第一次打开时不出现 spinner(DESIGN §9;预取 + 弹窗本身的进场即可)。
|
||||
- 老档案城市名逐字不变。
|
||||
- **风险**:弹窗第一次打开慢一个块的下载(约 100 KB),需要预取。people 的老档案路径要有回归测试。
|
||||
|
||||
### F2 | 首页首屏静态带着整个校正面(74 个文件)
|
||||
- **位置**:`src/app/(app)/page.tsx`,第 25 行静态 `import { ConversationalBirthTimeRectification }`。它只在 `rectificationSurfaceOpen && rectificationCaseId` 时渲染(第 1347 行附近)。
|
||||
- **问题**:只有进入校正才用到的 v9 题池、精炼包、分歧面板、校正棋盘、交付卡、报告用的 `vedic-chart-svg` 等,全部进了 `/` 的页面块 `18d0n2myljhv9.js`(实测 110 KB gzip)。绝大多数人打开首页是普通对话。
|
||||
- **量化**
|
||||
- 可达性脚本(实测):只经这个组件可达的有 **74 个文件**,源码 620 KB,压缩后(minified)162 KB。
|
||||
- 按页面块的 gzip/minified 比例 0.30 推算,约 **48–52 KB gzip(估)**,占首屏 7%。
|
||||
- 页面块内体积前几位:`collection-question-pool` 14.5 KB、`refinement-packet` 13.8 KB、`divergence-panel` 12.2 KB、`rectification-agentic-chat` 10.1 KB、`rectification-board` 6.6 KB(minified,实测)。
|
||||
- **建议改法**
|
||||
1. 换成 `next/dynamic(() => import(...), { ssr: false })`,并加一个 `preloadRectificationSurface()`。
|
||||
2. 三处调用它:`openRectificationSession` 开始请求时;bootstrap 落地会话是校正型、或 URL 指向校正时,在揭幕前 await;校正 offer 卡出现时。
|
||||
3. `useRectificationSurface`、consent、composer guard 等首页本身要用的 hook 留在原处。
|
||||
- **验收标准**
|
||||
- 首屏 gzip-9 降 ≥ 40 KB,`/` 仍 `○ Static`。
|
||||
- 进入校正与刷新校正 URL 后,揭幕后不出现 spinner 或空白帧。
|
||||
- `docs/testing` 里的校正走查清单不变。
|
||||
- 若有源码文本测试在锁 `page.tsx` 的 import 写法,按三栏改。
|
||||
- **风险**:`page.tsx` 不得增长,`dynamic` 加预取放进 `lib/` 新文件。须核对 Python 源码合同测试(`tests/test_daily_and_rectification_entrypoints.py` 等)有没有锁这一行。
|
||||
|
||||
### F3 | 回答流式期间,整个 Home 和右侧本命盘面板每帧重渲染
|
||||
- **位置**
|
||||
- `src/app/(app)/page.tsx:Home`,第 247 和 265 行:`useSessionStore(useShallow(...))` 订阅了 `streamingReply`。
|
||||
- `src/lib/stream-frame-buffer.ts`:每个 rAF 提交一次 `setStreamingReply`。
|
||||
- `src/components/chat-chart-frame.tsx:ChatChartFrame`:`children` 每次都是新的,`Split` 没有 memo。
|
||||
- `src/components/chat-natal-chart.tsx:PanelBoard`,以及 `ChartBoardCard`、`ChartInspector`、`ChartGrahaCard`、`ChartTags`,全都没有 memo。
|
||||
- **问题**
|
||||
1. 流式文本每帧换一个新对象,Home(一个 1,235 行的函数)连同里面十来个自定义 hook 整体重跑。随之重渲染的有:顶栏、`ChatProfilePicker`、`CreditBalanceButton`、`ChatComposer` + `ModelSelector`(toolbar 每次是新元素)、`OnboardingHomeBridge`、`DailyStarlanguageBinder`、`AccountDialogOverlay`,以及每次重建的 `accountOverlayModel`。
|
||||
2. 桌面端本命盘面板默认打开(#114 Q8 A)。它挂在 `ChatChartFrame` 下,于是 `useChartBoard`、`useTransitAt` 和整张星盘 SVG 跟着每帧重画。
|
||||
3. BUG-1329 只解决了侧栏重注册;`ChatTranscript` 和 `SettledMessageList` 已经 memo,历史消息不受影响。
|
||||
4. 同类问题:新手引导打字机 `setPresetMessageLength` 每 26 ms 一次(第 929 行),也落在 Home 顶层,约 38 次/秒整页重渲染。
|
||||
- **量化**
|
||||
- 每秒最多 60 次 Home 提交。按 DESIGN 放字节奏 240 字/秒推算,一条 1,500 字的回答约 375 帧,即 Home 375 次、星盘面板 375 次重渲染(估)。
|
||||
- 测法:仿 `tests/home-sidebar-rerender.test.tsx`,用 `<Profiler>` 包住 `ChatChartSplit`(或 `PanelBoard`)和 `ChatComposer`,推 60 帧 `setStreamingReply`,数 commit 次数和 `actualDuration`。在桌面 Chrome 上用 React DevTools Profiler 录一次长回答,可以直接看到 PanelBoard 的每帧耗时。
|
||||
- **建议改法(一张小单)**
|
||||
1. `ChatChartSplit` 用 `memo` 包住。它的 props 是 zustand 的稳定 setter 加上 open/width/subjectId,包一层即可隔离。
|
||||
2. 新建 `components/chat-streaming-transcript.tsx`:它自己用 selector 订阅 `streamingReply`(按 activeSessionId 过滤),再把流式字段喂给 `ChatTranscript`。Home 只订阅派生的布尔值,例如 `hasStreamingText`,供停止按钮文案用。
|
||||
3. 打字机长度下放到 `HomeOnboardingShellSlot` 内部。
|
||||
- **验收标准**
|
||||
- Profiler 测试:60 帧内 PanelBoard 提交 0 次、ChatComposer 提交 0 次、Home 提交 ≤ 2 次(开始和结束)。
|
||||
- 正文逐字一致,停止按钮文案切换不变。
|
||||
- `page.tsx` 行数不增。
|
||||
- **风险**:停止按钮文案依赖「已有正文」,派生布尔要准确。`useConversationScrollAnchor` 跟随底部依赖内容高度变化,不依赖 Home 重渲染,需要回归它的测试。
|
||||
|
||||
### F4 | 首页首屏静态带着 Markdown 渲染全家(react-markdown + remark-gfm)
|
||||
- **位置**:`src/components/chat-markdown-view.tsx`,经 `chat-message-content` → `ChatTranscript`;校正面也经同一组件。
|
||||
- **问题**:空白首页(登录后、裸 `/` 的默认落点,BUG-1052)没有任何消息,却要下载和解析 micromark、mdast、hast 这一整套。
|
||||
- **量化**:块 `1stba0waqt7fg.js` 实测 **42,332 B gzip-9**,minified 141 KB,几乎全是 markdown 依赖。约占首屏 6%。
|
||||
- **建议改法**
|
||||
1. `ChatMarkdownView` 内部按需 `import("react-markdown")` 和 `remark-gfm`,并提供 `preloadChatMarkdown()`。
|
||||
2. 三处调用它:bootstrap 落地会话有消息时,在揭幕前 await;用户按发送的那一帧(回答首字通常要几秒);校正面预取(F2)时。
|
||||
3. 未加载完成前用现有的纯文本段落渲染(`chat-message-paragraphs`)。按上面的预取,正常路径碰不到这个分支。
|
||||
- **验收标准**
|
||||
- 首屏 gzip-9 降 ≥ 40 KB。
|
||||
- `?c=` 打开带消息的会话,揭幕时 Markdown 已渲染(不先出纯文本再跳格式)。
|
||||
- 流式首帧照常。
|
||||
- **风险**:揭幕时序。必须在 bootstrap 里 await,否则会违反「一次揭幕」。
|
||||
|
||||
### F5 | 首屏几件「点了才用」的小依赖(合计约 27 KB gzip)
|
||||
- **位置与问题**
|
||||
| 依赖 | 位置 | 体积(minified,实测) | 实际用途 |
|
||||
| --- | --- | ---: | --- |
|
||||
| date-fns 的 parse/format 及整套 en-US locale 解析器 | `src/lib/birth-time-intake-model.ts:1`,`parseBirthDate`、`formatBirthDate` | 45.4 KB,约 12 KB gzip(估) | 只为 `yyyy-MM-dd` 一个格式 |
|
||||
| better-auth 客户端(better-fetch、nanostores) | `src/hooks/use-profile-onboarding.ts:63`、`src/components/account-deletion-gate.tsx:18` | 31.7 KB,约 10 KB gzip(估) | 只为点「退出」时的 `signOut()` |
|
||||
| `consultation-report-export` → `report-public-projection` | `src/hooks/use-shell-session-actions.ts:8`(外壳,所有页面首屏都有) | 15.3 KB,约 4.5 KB gzip(估) | 只为点「分享」 |
|
||||
|
||||
另外 `page.tsx:120` 那一行 `import { consultationReportMarkdown }` 在 Home 里是未使用的 import。
|
||||
- **建议改法**
|
||||
1. 用十来行手写 `yyyy-MM-dd` 解析替换 date-fns:正则、`new Date(y, m-1, d)`、三段回写校验,保留年份上下限。日历组件里的 date-fns 随懒加载的 react-day-picker 走,不受影响。
|
||||
2. `signOut` 改成点击时 `await import("@/modules/identity/client")`。
|
||||
3. `shareTranscript` 改成点击时动态 import,并删掉 page.tsx 里未使用的 import。
|
||||
- **验收标准**
|
||||
- 首屏 gzip-9 降 ≥ 20 KB,所有次级页首载同降约 15 KB(外壳的两件)。
|
||||
- `parseBirthDate` 现有测试全过,补上 2 月 30 日、0000 年、前导空格三条用例。
|
||||
- 退出和分享行为不变。
|
||||
- **风险**:低。手写解析必须与 date-fns 的严格回写校验等价,测试先在旧实现上跑一遍对齐。
|
||||
|
||||
### F6 | 次级页冷开:bundle → /api/account → 业务接口,三段串行
|
||||
- **位置**
|
||||
- `src/hooks/use-chart-page.ts`,第 76 行的 effect 以 `subjectReady` 为门。
|
||||
- `src/lib/current-subject.ts`:`catalogReady` 要等 `/api/account` 绑定账户之后才为真。
|
||||
- `src/lib/boot-early-reads.ts:bootEarlyReadsScript` 只在 `pathname === "/"` 时发早读。
|
||||
- `/ephemeris`、`/reports` 同样形状。
|
||||
- **问题**:刷新或直接打开 `/chart`,要等 JS 执行完才发 account 和 chart-profiles,再等它们返回才发 `/api/chart-view`(重计算)。HTML 阶段就能知道的读(account、chart-profiles、按本机记住的人物猜的 chart-view 或 ephemeris)全部推迟到 bundle 执行之后。
|
||||
- **量化**:串行段 = `/chart` 首载 579 KB JS 的下载与执行 + 1 个 RTT(account)+ chart-view 服务端耗时(估)。测法:在 staging 用 Chrome 冷开 `/chart`,看 Network 瀑布里 `/api/chart-view` 的开始时间相对 `/api/account` 的结束时间。
|
||||
- **建议改法**:把早读脚本扩到 `/chart`、`/ephemeris`、`/reports`。account、chart-profiles 两条与首页共用;再按本机记住的人物猜一条页面主读。`useChartPage` 首读用 `takeBootRead` 接过它;人物猜错就丢弃重读,与 BUG-1127 的会话猜测同一套红线。做完 F1 之后再做,这样 bundle 那段也变短。
|
||||
- **验收标准**
|
||||
- 冷开 `/chart`:`/api/chart-view` 的请求开始时间早于 `/api/account` 的响应结束时间(瀑布截图)。
|
||||
- 换人或换账户后不显示别人的盘(沿用 BUG-1127 红线 3、8 的测试形状)。
|
||||
- **风险**:中。猜错人物会白算一次 chart-view,占重计算名额(`JYOTISH_HEAVY_COMPUTE_CONCURRENCY=2`)。可以限定只在本机记住的人物 = self 时才猜。
|
||||
|
||||
### F7 | 打开 `?c=<id>` 时,会话详情要排在第二轮
|
||||
- **位置**:`src/lib/home-bootstrap-run.ts:resolveLanding`,第 436 行的 `fetchSessionDetail`;`src/lib/home-cloud-sync.ts:resolveLookupBootstrap`,第 576 行。
|
||||
- **问题**:分享链接、侧栏历史、在对话里刷新,都走 `?c=`。会话 id 在 HTML 阶段就已知,但 `GET /api/sessions/:id` 要等账户、会话列表、模型目录、active status 全部返回之后才发,于是揭幕多等一轮。id 不在第一页列表里时,走的是 `lookupSessionById`,同一个 URL,同样排在第二轮。
|
||||
- **量化**:揭幕多 1 个 RTT + 会话详情的服务端耗时(估,手机网络约 150–400 ms)。测法:staging 冷开 `/?c=<自己的会话>`,看瀑布里 `/api/sessions/<id>` 的开始时间是否晚于 `/api/sessions?limit=` 的结束时间。
|
||||
- **建议改法**
|
||||
1. 早读脚本在 `?c=` 存在时多发一条 `/api/sessions/<id>`,键名为 `sessionDetail`,与 id 绑定。
|
||||
2. `fetchSessionDetail` 和 `lookupSessionById` 先 `takeBootRead`,没有再自己发。
|
||||
3. 401 和 404 的处理照旧。
|
||||
- **验收标准**
|
||||
- 瀑布里会话详情与 account 同一轮发出。
|
||||
- 打开别人的或已删除的会话,提示不变(SESSION_MISSING / LOOKUP_FAILED 两条文案)。
|
||||
- 现有 home-bootstrap 测试全过。
|
||||
- **风险**:低。早读结果必须与 id 和账户绑定,账户变化时丢弃(同 BUG-1040)。
|
||||
|
||||
### F8 | 档案页、合盘页各自重拉一遍外壳已有的数据,合盘页还是两轮串行
|
||||
- **位置**
|
||||
- `src/components/people/people-page.tsx:load`,第 126 行:`fetchAccount()` + `fetch("/api/chart-profiles")`。
|
||||
- `src/components/synastry/synastry-picker.tsx:load`,第 68 行先 `await /api/product-access`,第 90 行再 `fetchAccount()` + `/api/chart-profiles`。
|
||||
- 外壳已经由 `session-list-context.loadSessionList` 和 `current-subject.loadSubjectCatalog` 拿到同样的两份数据。
|
||||
- **问题**
|
||||
- 同一份数据有两套来源:外壳 store 和页面自己的 state。每次进页都多 2 个请求,从侧栏点进来(外壳数据早已就绪)也一样。
|
||||
- 合盘页 product-access → account 和 profiles 是 2 轮串行,其实三者互不依赖。
|
||||
- 档案页在本页改人以后,还要自己 `bindCurrentSubjectAccount` 去同步外壳。
|
||||
- **量化**:每次进入 `/people` 或 `/synastry` 多 2 个请求(实测,代码路径)。合盘页揭幕多 1 个 RTT(估)。
|
||||
- **建议改法**
|
||||
1. 合盘页把三条请求改成 `Promise.all`。
|
||||
2. 两页的 account 改读 `useSessionList().account`,档案列表改读 `peekSubjectCatalog()` / `loadSubjectCatalog()`。需要新鲜数据时调 `invalidateSubjectCatalog()` 再读,不再各自 fetch。
|
||||
- **验收标准**
|
||||
- 从侧栏进 `/people`、`/synastry`:Network 里没有新的 `/api/account`、`/api/chart-profiles`(外壳已就绪时)。
|
||||
- 合盘页的三条请求同时发出。
|
||||
- 改人、加人、删人后,侧栏人物切换器与档案页一致。
|
||||
- **风险**:中。档案页的排序字段 `archive_order` 要确认外壳 catalog 也带着;没带就先补进 catalog,不要另起一套。
|
||||
|
||||
### F9 | Motion(约 85 KB gzip)在每个页面挂载后立刻下载,与首屏接口抢带宽
|
||||
- **位置**:`src/components/shell-motion.tsx:useMotionReact`,第 18 行,在 `useEffect` 里直接 `loadMotionReact()`。调用方是侧栏的 `SidebarCurrentHighlight`,每个页面都有。
|
||||
- **问题**:#104~#111 规定 Motion 按需加载。首屏里确实没有它,但侧栏的当前项高亮在挂载那一刻就拉下 motion-dom 和 framer-motion。首页揭幕前的加载屏期间,它与 account、sessions、models 早读和会话详情同时下载。
|
||||
- **量化**:`33k991bwerupc.js` 59,466 B + `2l8aqrmhvdn58.js` 22,010 B + `076a3yxomi2av.js` 3,417 B,gzip-9 合计约 **84.9 KB**(实测块体积;挂载即下载是读代码得出)。
|
||||
- **建议改法**:`useMotionReact` 改为等 `document.documentElement.dataset.hydrated === "1"`(首页揭幕标记)或 `requestIdleCallback` 之后再加载。次级页用 idle。滑动高亮在 Motion 到手前本来就有静态降级。
|
||||
- **验收标准**
|
||||
- 首页冷开的瀑布里,motion 块在揭幕之后才开始下载。
|
||||
- 侧栏高亮第一次切页仍会滑动(切页通常晚于 idle)。
|
||||
- 减少动态效果设置照旧生效。
|
||||
- **风险**:低。
|
||||
|
||||
### F10 | `page.tsx` 与大 hook 的状态纠缠(结构债,影响后续每一单的改动半径)
|
||||
- **位置**:`src/app/(app)/page.tsx:Home`,第 207–1441 行,单函数 1,235 行。
|
||||
- **问题(实测计数)**
|
||||
- Home 内有 27 个 `useState`、21 个 `useEffect`、38 个 `useRef`。
|
||||
- `useConsultationRun({...})` 传入 54 个参数(第 754–809 行),`useHomeShellRegistration` 25 个,`useProfileOnboarding` 10 个。
|
||||
- 有 **11 个 ref 在 Home 里只出现 2 次**(声明一次、传给 hook 一次),本质上是那个 hook 的私有状态:`cancellationRequests`、`cancellationFeedbackRequest`、`stoppedRequestAwaitingSettlement`、`stoppedSessionPersistence`、`modelPersistence`、`modelSyncFailures`、`modelSelectionVersions`、`sessionDetailInFlight`、`accountRefreshGuard`、`birthTimeRevisionPendingRef`、`onboardingPaywallShown`。
|
||||
- 根源是 AGENTS §6 的「参数式 hook 内部保持 0 个 React hook」,状态只能放在 Home 再层层传进去。
|
||||
- **量化**:见上面的计数。`useConsultationRun` 本身也有 1,147 行,其中 `send` 713 行。
|
||||
- **建议改法(不碰 §6 红线的一张小单)**
|
||||
1. 在 `lib/` 新建 `createConsultationRunRefs()`,返回一个普通对象,装上面 11 个可变容器。Home 只用一个 `useRef(createConsultationRunRefs())`,传一次。参数从 54 降到 44 左右,行为零变化。
|
||||
2. 若要更进一步让 hook 自己持有 state,就要在任务书决策记录里明确推翻 §6 那一条,另立单。
|
||||
- **验收标准**
|
||||
- `page.tsx` 行数减少;`useConsultationRun` 参数 ≤ 45。
|
||||
- 全量前端测试名单与基线一致(frontend/AGENTS.md 的名单比对法)。
|
||||
- tsc 和 lint 0 error。
|
||||
- **风险**:低到中。源码文本测试可能锁着 ref 名字,要用 `git grep -- tests/ frontend/` 全量核对。
|
||||
|
||||
### F11 | 同一件事好几套实现
|
||||
| 编号 | 两套(或更多)实现 | 位置 | 影响 | 合并方向 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| F11a | 会话「加载更多」 | `use-session-management.ts:loadMoreSessions`(369 行起,带 modelCatalog、`cloudCreatedIds`、代际号)与 `use-shell-session-actions.ts:loadMoreSessions`(140 行起,不带这三样)。外壳通过 `hooks?.onLoadMore` 二选一 | 首页内外行为不一致:外壳版用 `readSessions(..., null)`,没有模型回落;修 bug 要改两处 | 只留首页版的逻辑,挪进 `lib/session-page-load.ts`,两边调用 |
|
||||
| F11b | 读单个会话 | `home-cloud-sync.ts:fetchSessionDetail`(525 行)与 `lookupSessionById`(548 行),同一个 URL、同样的解析,只差 404 的处理 | 小;F7 的早读要两处都接 | 合成一个返回 `found/missing/unavailable` 的函数 |
|
||||
| F11c | 北印度式星盘 SVG | `personal-report/vedic-chart-svg.tsx`(411 行;报告、校正棋盘、星盘页骨架、chart-vedic-tab 在用)与 `chart-board/chart-board-svg.tsx`(373 行;星盘页、聊天面板在用),共用 `north-indian-chart-geometry` | 视觉改动要做两遍,两套容易走样 | 大单,不建议现在做;先在 DESIGN 里写明哪一套是权威 |
|
||||
| F11d | 账户与人物列表来源 | 外壳 store 与 people / synastry 页自取 | 见 F8 | 见 F8 |
|
||||
| F11e | `onboarding-run.ts` | 既在首屏块(经 `onboarding-progress-ring`),又整份复制进懒块 `21f3q`(9.5 KB minified) | 约 3 KB gzip(估) | `playRing` 改动态 import,或让懒块直接复用 |
|
||||
|
||||
### F12 | 只有测试引用、或没人引用的模块(死代码残余)
|
||||
可达性脚本从全部 app 入口(page、layout、route、error 等 150 个)和 `frontend/scripts/` 出发,计入动态 import。不可达的文件有 23 个、959 行(实测)。其中真正值得删的:
|
||||
- **只被测试引用**
|
||||
- `lib/birth-time-choice-question-model.ts`(50 行)
|
||||
- `lib/birth-time-guided-terminal.ts`(29 行)
|
||||
- `lib/birth-time-guided-turn-identity.ts`(3 行)
|
||||
- `stores/people-store.ts`、`stores/account-store.ts`(各 12 行;只有 `ui-kit-stores.test.ts` 用)
|
||||
- `lib/motion/provider.tsx`(16 行,静态 import `motion/react`。将来谁一引用,Motion 就回到首屏)
|
||||
- **无人引用**
|
||||
- `components/chart-page/chart-basics-tab.tsx`(20 行,#114 改版后遗留)
|
||||
- `components/error-toast.tsx`(17 行)
|
||||
- shadcn 生成后从未使用的 `ui/field`、`avatar`、`empty`、`switch`、`kbd`、`separator`、`skeleton`(共 543 行)。`skeleton` 与 DESIGN §9 的「揭幕后不得出现骨架」冲突,留着容易被误用。
|
||||
- **已知保留**,dead-code 进度记录里写过理由,不再提:`reader-appendix-language.ts`、`personal-report-longform-download.ts`。
|
||||
- 建议一张小单删除,连同对应测试。删前用 `git grep -n <名> -- tests/ frontend/` 核对。
|
||||
- 验收:可达性脚本不可达文件数 ≤ 2;测试名单只少被删模块的用例,并在三栏表写明。
|
||||
- 风险:低。
|
||||
|
||||
### F13 | 首屏 CSS:整站样式一次性阻塞渲染(低优先)
|
||||
- **位置**:`src/app/globals.css`(7,000 行,块 `3u1r1j1aer98v.css` 实测 57,259 B gzip-9);`src/app/fonts/serif-sc/serif-sc.css`(30 个 `@font-face`,66 KB 原文,实测 22,547 B gzip-9),两者都由 `site-styles.ts` 在每个页面引入。
|
||||
- **问题**
|
||||
- globals 里按类名前缀粗分,报告、合盘、档案、星历四类规则约 82 KB 原文(约 25%),首页用不到,但都是阻塞渲染的 CSS。
|
||||
- 宋体切片的 `unicode-range` 逐字列出码位,单这份声明就占 22.5 KB gzip。
|
||||
- **量化**:首页可省约 12–15 KB gzip CSS(估)。宋体声明若按码位连续段重新分片,可省 10–15 KB(估;会改变每片命中率,BUG-1128 要重新评估)。
|
||||
- **建议**:先做 globals 拆分:`personal-report-*`、`synastry-*`、`people-*`、`ephemeris-*` 四段挪到各自路由组件 import 的 css。宋体不动,记入 BLOCKED 或进度记录备查。
|
||||
- **验收**:`/` 首屏 CSS gzip 降 ≥ 10 KB;四个页面在 `docs/testing` 的截图对照无差。
|
||||
- **风险**:中。CSS 顺序与优先级可能变化,必须逐页截图对照。
|
||||
|
||||
---
|
||||
|
||||
## 二、超长文件 / 函数(前 10,实测)
|
||||
|
||||
**文件**(`frontend/src`,行数)
|
||||
| # | 文件 | 行数 | 说明 |
|
||||
| ---: | --- | ---: | --- |
|
||||
| 1 | `lib/personal-report-generation.ts` | 3,740 | 服务端 |
|
||||
| 2 | `lib/rectification-agentic/v9/method-followup.ts` | 3,574 | |
|
||||
| 3 | `lib/rectification-agentic/v9/answer-choice.ts` | 2,641 | |
|
||||
| 4 | `mastra/rectification-v9-tools.ts` | 2,638 | |
|
||||
| 5 | `lib/rectification-agentic/v9/tool-service.ts` | 2,169 | |
|
||||
| 6 | `app/api/consult/route.ts` | 1,855 | |
|
||||
| 7 | `mastra/consultation-tools.ts` | 1,777 | |
|
||||
| 8 | `app/(app)/page.tsx` | 1,441 | 客户端,冻结中 |
|
||||
| 9 | `lib/rectification-agentic/v9/collection-question-pool.ts` | 1,404 | 客户端首屏,见 F2 |
|
||||
| 10 | `hooks/use-consultation-run.ts` | 1,308 | 客户端 |
|
||||
|
||||
另:`globals.css` 7,000 行。
|
||||
|
||||
**函数**(TypeScript AST,按函数体行数)
|
||||
| # | 函数 | 位置 | 行数 | 说明 |
|
||||
| ---: | --- | --- | ---: | --- |
|
||||
| 1 | `createRectificationV9Tools` | `mastra/rectification-v9-tools.ts:1003` | 1,596 | |
|
||||
| 2 | `POST` | `app/api/consult/route.ts:277` | 1,579 | 内含 `runTurn` 1,326 行、`runAgenticConsultation` 621 行;10-13 体检的 consult-route-split 单仍是待领取 |
|
||||
| 3 | `Home` | `app/(app)/page.tsx:207` | 1,235 | |
|
||||
| 4 | `useConsultationRun` | `hooks/use-consultation-run.ts:162` | 1,147 | 内含 `send` 713 行 |
|
||||
| 5 | `buildMethodFollowupPlan` | `lib/rectification-agentic/v9/method-followup.ts:2286` | 1,025 | |
|
||||
| 6 | `streamAgentResponse` | `lib/stream-agent-response.ts:505` | 704 | |
|
||||
| 7 | `EmailOtpLogin` | `components/email-otp-login.tsx:87` | 624 | 客户端组件 |
|
||||
| 8 | `streamV9Attempt` | `lib/rectification-agentic/v9/agent-run-attempt.ts:79` | 621 | |
|
||||
| 9 | `RectificationAgenticChat` | `components/rectification-agentic-chat.tsx:113` | 617 | |
|
||||
| 10 | `useSessionManagement` | `hooks/use-session-management.ts:97` | 512 | |
|
||||
|
||||
本轮可执行的只有 F10(Home 和 `useConsultationRun` 的参数与 ref 收拢),其余服务端大函数属于另一半体检的范围。
|
||||
|
||||
---
|
||||
|
||||
## 三、已解决、不用再提(本次核对到的状态)
|
||||
|
||||
| 事项 | 状态与证据 |
|
||||
| --- | --- |
|
||||
| 侧栏每帧重注册(BUG-725/1329,#59) | 已合入 staging(`635094315`)。注册函数稳定,60 帧注册 0 次。F3 是另一件事:Home 本体和星盘面板的每帧重渲染。 |
|
||||
| 前端死代码(BUG-1342,#63) | 已合入(`1f9947a0d`、`3e20c125b`)。删掉 17 个文件约 3,000 行和 3 个无调用方接口。F12 只是残余的 23 个小文件。 |
|
||||
| 首页首屏串行与缓存(BUG-1127~1130) | 已合入。早读改 async data: 脚本(`c2265dabb`),宋体切片永久缓存(`8112f62b5`),加载屏改黑体(`80ea7b0bc`),城市表、holdout、引导壳移出首页首屏(`b743b16b0`)。本次核对 `/` 首屏确实没有城市表;F1 是同一问题出现在 `/chart`、`/people`。 |
|
||||
| shadcn / Motion / dnd-kit / resizable 迁移(#104~#111) | 已合入。首屏里 motion-dom、framer-motion、@dnd-kit、react-day-picker、drawer、dropdown 都在懒块(实测,均不在 `index.html` 引用的块里);react-resizable-panels 不可达。首屏 694,941 → 702,985(+1.16%)在 ±2% 内。F9 只说 Motion 懒块的下载时机。 |
|
||||
| 本地 PG 适配层每查询往返与现查表结构(BUG-1327/1328,#58) | 已合入(`4c3485bc2`)。`local-postgres-client-core.ts:columnTypes` 已带 `metadataCache`。 |
|
||||
| 前端 SupabaseClient 假类型(#71,BUG-1351) | 第一阶段已合入(`1de3cc307`,`lib/db/data-client.ts`)。代码里仍有 212 处 `SupabaseClient` 字样,属于 #71 后续阶段,不另提。 |
|
||||
| 接口定义合并(#72) | 第一阶段已合入(`5d452a4ff`)。 |
|
||||
| 源码正则测试约 105 个文件 | 已在 10-13 体检里立项,本轮不重复。F2、F5、F10 动手前须按 frontend/AGENTS.md 全量 grep。 |
|
||||
| 对话历史消息的 memo | `ChatTranscript`、`SettledMessageList`、`HistoryMessageEntry`、`ChatMessageRow` 已 memo,历史消息不随流式重渲染。 |
|
||||
|
||||
## 四、建议顺序(都是独立小单)
|
||||
1. **F1**:/chart、/people 降约 100 KB,改动最小。
|
||||
2. **F3**:流式卡顿。
|
||||
3. **F2 + F4**:首屏约 −90 KB。两者都改首页揭幕时序,串行做,先 F2。
|
||||
4. **F5**:约 −27 KB,零风险。
|
||||
5. **F7 → F6**:接口早读,F6 排在 F1 之后。
|
||||
6. **F8、F9**。
|
||||
7. **F10、F11a、F12**:清理。
|
||||
8. **F13** 最后,或只记账不做。
|
||||
|
||||
预计 F2 + F4 + F5 做完后首页首屏 gzip-9 约 595 KB(估,−15%)。F1 做完后 `/chart` 约 471 KB(估,−19%)。
|
||||
Reference in New Issue
Block a user