Files
Jyotisha/docs/research/code_health_audit_2026_10_28.md
T

48 KiB
Raw Blame History

代码体检 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%)。