diff --git a/BLOCKED.md b/BLOCKED.md index 22905f0e..05ce9560 100644 --- a/BLOCKED.md +++ b/BLOCKED.md @@ -3,3 +3,13 @@ - 真实收信端到端验收:执行环境没有可识别的 staging 测试邮箱/收件箱变量,仓库只记录发信配置而未提供受控测试邮箱。按任务硬规则不使用他人邮箱;代码、测试和部署继续,部署后的注册、验证码登录与忘记密码真实收信步骤待具备受控邮箱后补验。 - PostgreSQL 事务反向测试:当前执行环境没有 `docker`、`postgres`、`initdb`、`psql`、Podman/Colima/Lima。`frontend/tests/admin-database.test.ts` 已实现审计触发器故意失败并断言兑换码行数仍为 0 的红灯证据,但本地执行在启动 fixture 前以 `spawnSync docker ENOENT` 阻塞;交由 exact-SHA staging quality gate 的 Docker 环境运行。全量 `npm test` 因同一缺失 Docker 共阻塞 11 项数据库/部署测试,另有 1 项既有真实 DOM 测试因缺 Playwright headless Chromium 阻塞;其余 1031 项通过,skipped/todo=0。 - staging 两角色浏览器冒烟:已确认受控 admin 测试账号存在且是 `user,admin`,但当前执行环境没有其密码或已登录会话;也未提供受控 viewer 账号。不得读取/猜测凭据或使用他人账号。已完成匿名 shell、5 个资源 401、写请求 401 的服务端冒烟;admin/viewer 登录后浏览器冒烟待授权人员提供受控会话后补验。 + +## React Compiler 接管 page.tsx(2026-08-17,分支 codex/react-compiler-20260817) + +- **`Home` 的编译失败原因无法定位,需要授权新增依赖。** Next 16.3.1 的 Rust 版 React Compiler 拒编 `page.tsx` 的 `Home`(2730 行),且不报任何错误或警告。`panicThreshold: "all_errors"` 在 Rust 版下形同虚设(试过两次,构建既不报错也不打印诊断),因此拿不到「哪一行挡住了编译器」。唯一能出诊断的路是换 Babel 版(`experimental.turbopackRustReactCompiler: false`),但它需要新增依赖 `babel-plugin-react-compiler`(next 的可选 peer,未装;Next 只内置了 compiler runtime,没内置这个 plugin),撞任务书「不新增依赖」,故未做。想继续定位需要授权装这个依赖,或等上游 Rust 版补上诊断输出。 +- **`Home` 要真正受益很可能得拆组件,属业务代码重构,本轮明令禁止。** 证据是同一文件里两个小组件(原始行 760、815)被正常编译、全项目另有 44 个函数被编译,唯独这个 2730 行、24 个 `useState`、18 个 `useEffect` 的函数被拒。任务书要求「哪处代码挡住了编译器就写进 BLOCKED.md,不要自行改写」,故 `frontend/src/**` 一行业务代码未动(仅反向验证时临时加删一行 `"use no memo"`,已还原,`git diff` 为空)。 +- **待决策:是否为了 `page.tsx` 之外的 44 个组件而保留 `reactCompiler: true`。** 本轮按任务 2 止损条款已回滚该配置。数据是:构建耗时无可测量变化(噪声内),`/` 首屏 JS gzip +12031 B / +2.50 %,换来 44 个函数自动记忆化,其中 `app-sidebar`(112 槽)、`sidebar-session-row`(73 槽)、`birth-date-picker`(58 槽)与三个引导 hook 都在 `/` 上渲染。这 44 个的实际运行时收益本轮未测量(无渲染基准),所以不替决策者拍板。重新开启只需在 `frontend/next.config.ts` 顶层加 `reactCompiler: true`、`experimental` 里加 `turbopackRustReactCompiler: true`。 +- **`compilationMode: "all"` 不可用,会破坏构建。** 它会编译模块作用域的普通回调,`frontend/src/components/birth-time-intake.tsx:22` 的 `Array.from({ length: 24 }, (_, index) => ...)` 被插入 `useMemoCache`,预渲染 `/` 时抛 `TypeError: Cannot read properties of null (reading 'useMemoCache')`。这是该模式本身的性质(官方标注为不安全),不是本仓库代码的缺陷,未做任何改动。 +- **顺手活一律未做,登记在此:** 其一,`frontend/src/lib/skill-package-registry.ts:523` 的 `readFileSync(currentRegistryPath, "utf8")` 触发 Turbopack 构建警告「Dynamic filesystem access causes tracing of the whole project」,会把整个项目(含 `public/`)打进 server 产物,影响部署体积;升级前后都存在,与本轮无关,未改。其二,`npx eslint` 有 4 个既有 `no-unused-vars` warning,分别在 `birth-time-candidate-result.tsx:143`、`birth-time-candidate-completion.ts:10,11`、`tests/identity-auth-factory.test.ts:48`,未改。其三,`eslint-config-next` 仍是 16.2.10、与 next 16.3.1 版本号不同步,但 eslint 实测 0 error,按「不许顺手升别的依赖」未动。 +- **测试环境噪声(非阻塞,已自行消化):** `frontend/tests/rectification-v9-database.test.ts` 的「v9 migration applies on a fresh database and re-applies idempotently」在全量并发下偶发失败(`database migration failed`,1 !== 0),单独重跑 7/7 通过、全量重跑 1592/1592 通过。靠 Docker 起临时 Postgres,判定为资源争用型 flake,与 Next 版本无关。未改任何测试文件。 +- **文件名偏离:** 任务书要求新建 `PROGRESS.md`,但根目录已有受版本控制的 `progress.md`(1025 行)且本机文件系统大小写不敏感,写 `PROGRESS.md` 等于覆盖清单外的文件,故进度记录落在 `PROGRESS-react-compiler-20260817.md`。 diff --git a/PROGRESS-react-compiler-20260817.md b/PROGRESS-react-compiler-20260817.md new file mode 100644 index 00000000..584944dd --- /dev/null +++ b/PROGRESS-react-compiler-20260817.md @@ -0,0 +1,207 @@ +# PROGRESS · React Compiler 接管 page.tsx 重渲染(codex/react-compiler-20260817) + +## 文件名偏离说明(先写在最前面) + +任务书要求新建 `PROGRESS.md`。本仓库根目录已有受版本控制的 `progress.md`(1025 行), +而本机文件系统大小写不敏感(已实测:`ls PROGRESS.md` 命中 `progress.md`,内容相同)。 +在根目录写 `PROGRESS.md` 等于覆盖 `progress.md`,而 `progress.md` 不在「只允许改」清单里。 +两条约束打架时,优先不破坏清单外的文件,因此进度记录落在 +`PROGRESS-react-compiler-20260817.md`。`BLOCKED.md` 无此冲突,按原样追加到根目录既有文件。 + +## 我理解的目标/顺序/最大风险(任务 0 要求,≤10 行) + +1. 目标:让 React Compiler(Rust 版)真正编译 `frontend/src/app/page.tsx` 的 `Home`, + 使这个 3738 行、24 个 useState / 18 个 useEffect、0 个手写记忆化的组件由编译器自动优化。 +2. 顺序:任务 0 复核基线 → 任务 1 升 Next 16.3.1(Rust 版编译器的前置) → 任务 2 开启配置并取证 + + 反向验证 → 任务 3 测构建代价 → 补 `docs/BUG_HISTORY.md` 并提交。 +3. 让步顺序:编译器真的生效 > 测试与构建全绿 > 构建速度。 +4. 最大风险:配置开了、构建绿了,但编译器对着这个超大组件静默放弃(bailout 不报错), + 于是「看起来做完了、实际零收益」。所以取证必须落在构建产物上,并用 `"use no memo"` 反向验证 + 证明取证方法不是恒为真。 +5. 次要风险:16.3 的其他变更牵连别处(按任务 1 止损条款回滚);构建变慢或首屏体积上涨。 +6. 硬红线:不手写 useCallback/useMemo;`frontend/src/**` 只读(注解模式的一行指令除外); + 测试与 CI 配置一行不碰;测试数只许 ≥1592 且 skipped=0。 + +## 任务 0 · 基线复核 + +worktree:`/Users/jesse/Downloads/Copse/astrology/.worktrees/react-compiler-20260817` +(`git worktree add -b codex/react-compiler-20260817 ../.worktrees/react-compiler-20260817 origin/staging`, +基点 `e8d201dd`)。`frontend/` 已跑一次 `npm install`(新 worktree 无 node_modules)。 + +| 复核项 | 任务书给的数 | 实测 | 结论 | +| --- | --- | --- | --- | +| 过滤后测试文件数 | 199 | 199 | 对上 | +| 测试通过数 | 1592 | tests 1592 / pass 1592 / fail 0 / skipped 0 / todo 0 | 对上 | +| `npx tsc --noEmit` | 干净 | 退出码 0,无输出 | 对上 | +| `page.tsx` 行数 | 3738 | 3738 | 对上 | +| next 当前版本 | 16.2.10 | 16.2.10 | 对上 | +| `npm view next version` | 16.3.1 | 16.3.1 | 对上 | +| `babel-plugin-react-compiler` | 未装 | 未装(Rust 版不需要) | 对上 | + +`npx react-compiler-healthcheck` 查实:包存在,`npm view react-compiler-healthcheck version` = `1.0.0`。 +但它是 Babel 版编译器的体检工具,跑的不是本次实际启用的 Rust 版编译管线, +两者的 bailout 判定可能不一致,healthcheck 通过不等于 Turbopack 里真编译了。 +因此取证采用任务 2 的第二种方法(构建产物里定位 `Home` 的 `_c(N)` 缓存槽), +它直接落在真实构建产物上,且能被 `"use no memo"` 反向验证推翻。 + +锁文件:`npm install` 如任务书预告的那样整体重排 —— 961 条依赖,键序由非规范变为 npm 规范的字典序, +另有 6 条(`@types/react-dom`、`escape-string-regexp`、`js-tokens`、`loose-envify`、`prop-types`、 +`react-is`)去掉了 `dev: true`(npm 重算了它们的 dev 可达性)。逐条比对确认: +包数量 961→961、无新增、无删除、无 `version`/`resolved`/`integrity` 变化。已单独成一个提交 +(`3371baca`),排在版本升级之前,这样升级那一笔的 diff 只有 490 行且逐行可读。 + +顺带记下 16.2.10 的构建参考点(升级前的退路数据,不是任务 3 的四个数): +`npx next build` 退出码 0、`real 39.02s`,`/` 首屏 18 个 JS chunk 共 gzip 490798 B = 479.3 KB。 +任务书给的 476.3 KB 参考值与此相差 0.6%,说明体积口径对齐了。 + +## 任务 1 · 升级到 Next 16.3.1(完成) + +`npm install next@16.3.1 --save-exact`。锁文件的真实变更 490 行,逐条核对为: +`next`/`@next/env`/`@next/swc-*` 16.2.10→16.3.1,加上 npm 自行重解析的传递依赖 +`sharp` 0.34.5→0.35.3、`@img/*` 一族、`@swc/helpers` 0.5.15→0.5.23、 +`postcss` 8.4.31→8.5.23(原先嵌在 `@tailwindcss/postcss` 下的副本被提升合并,故包数 961→962)。 +这些都是升级的传递结果,不是我顺手升的依赖;`package.json` 只动了 `next` 一行。 + +`eslint-config-next` 仍留在 16.2.10:与 next 版本号不同步,但 `npx eslint` 实测 0 error +(4 个 warning 全在我没碰过的 `src/`、`tests/` 文件里,升级前就有), +没有出现必须同步升级的理由,按「不许顺手升别的依赖」保持不动,只把这条记在这里备查。 + +验收: + +| 项 | 结果 | +| --- | --- | +| `npx tsc --noEmit` | 退出码 0,无输出 | +| `npx next build` | 退出码 0(两次,均绿) | +| 测试 | tests 1592 / pass 1592 / fail 0 / cancelled 0 / skipped 0 / todo 0 | + +测试第一遍出现过 1 个失败:`tests/rectification-v9-database.test.ts` 的 +「v9 migration applies on a fresh database and re-applies idempotently」报 +`database migration failed`(1 !== 0)。它靠 Docker 起临时 Postgres,与 Next 版本无因果关系。 +单独重跑该文件 7/7 全过,全量重跑 1592/1592 全过,判定为全量并发下 Docker 资源争用导致的 flake。 +没有改任何测试文件,也没有加 skip:两次原始输出都在对话里贴了。 + +## 任务 2 · 开启编译器并取证(结论:编译器拒编 `Home`,按止损条款回滚配置) + +### 配置本身是对的 + +`reactCompiler` 确认是顶层选项、`turbopackRustReactCompiler` 确认在 `experimental` 里, +依据是 16.3.1 自带的类型定义 `node_modules/next/dist/server/config-shared.d.ts`: +第 1399 行 `reactCompiler?: boolean | ReactCompilerOptions`(`NextConfig` 顶层)、 +第 1092 行 `turbopackRustReactCompiler?: boolean`(`ExperimentalConfig` 内)。 +任务书这一点是对的,网上 `experimental.reactCompiler` 的写法确实是 Next 15 旧写法。 +按此写入后构建打印 `✓ turbopackRustReactCompiler`,`authInterrupts`、`optimizePackageImports`、 +`output`、`turbopack`、`outputFileTracingRoot`、`outputFileTracingIncludes` 全部逐项保留。 + +### 取证方法 + +用任务书的第二种取证法(构建产物定位 `Home` + 找编译器注入的缓存槽),不用 healthcheck,理由见任务 0。 +实现细节:`Home` 在产物里被压缩改名,`function Home(` 搜不到,所以改用 source map 反查 —— +`.next/server/chunks/ssr/_0kxuuj2._.js.map` 的 `sources` 里有 `frontend/src/app/page.tsx`, +解 VLQ mappings 就能把每个生成位置映射回原始文件与行号。两个脚本都放在 `/tmp`,不进仓库: +`/tmp/locate_home.mjs`(定位 `page.tsx:1002` 的 `Home` 并打印生成代码)、 +`/tmp/slot_owners.mjs`(把每个缓存槽 `_c(N)` 归属到原始文件,给出「每文件被编译函数数」表)。 +缓存槽在压缩后的真实形态是 `(0,X.c)(N)` 而不是 `_c(N)`,正则要匹配这个形态。 + +### 六次尝试的结果(全部原始输出已贴在对话里) + +| # | 配置 | `page.tsx` 被编译函数 | `Home` | 构建 | +| --- | --- | --- | --- | --- | +| 1 | `reactCompiler: true` | 2 | 未编译 | 退出码 0 | +| 2 | `{ panicThreshold: "all_errors" }` | 2 | 未编译,且未报任何错 | 退出码 0 | +| 3 | 同上 + `page.tsx` 文件顶部 `"use memo"` | 2 | 未编译 | 退出码 0 | +| 4 | `{ compilationMode: "all" }` | 47 | 未编译 | **失败** | +| 5 | `{ compilationMode: "all", panicThreshold: "all_errors" }` | 47 | 未编译,且未报任何错 | **失败** | +| 6 | `{ compilationMode: "annotation", ... }` + `Home` 体内 `"use memo"` | 0 | 未编译 | 退出码 0 | + +关键读数: + +- 默认 `infer` 下全项目共 44 个函数拿到缓存槽,`page.tsx` 内部有 2 个小组件被编译 + (原始行 760、815,槽数 11 和 24),但 3738 行文件里那个 2730 行的 `Home` 没有。 + 所以不是「文件没进编译器」,是「`Home` 这一个函数被拒」。 +- 第 4/5 次把 `compilationMode` 设成 `all`(绕过组件识别启发式,逼编译器编译每个函数), + `page.tsx` 被编译函数从 2 涨到 47,`Home` 仍然没有。既然 `all` 模式下不存在「没被识别成组件」 + 这回事,那就只剩一种解释:编译器尝试了 `Home` 并失败,然后按 `panicThreshold` 默认值 + `none`(文档原话「skips components which cannot be compiled」)静默跳过。 +- `panicThreshold: "all_errors"` 在 Rust 版下形同虚设:第 2、5 次都设了,构建既不报错也不打印 + 任何编译器诊断。所以拿不到「`Home` 到底哪一行挡住了编译器」的具体原因。 +- 第 4/5 次的 `compilationMode: "all"` 还会直接把构建搞坏,不能用:它会去编译模块作用域的普通回调, + 于是 `birth-time-intake.tsx:22` 的 `Array.from(..., (_, index) => ...)` 被插入 `useMemoCache`, + 预渲染 `/` 时炸 `TypeError: Cannot read properties of null (reading 'useMemoCache')`。 +- 第 6 次是领导指定的注解模式退路,也不成立。顺带纠正任务书一处技术细节: + `"use memo"` 是**函数体内**的指令,不是文件顶部指令,依据是 16.3.1 自带文档 + `node_modules/next/dist/docs/.../reactCompiler.md` 第 82-96 行的示例 + (`export default function Page() { 'use memo' ... }`)。我按正确位置放进 `Home` 体内第一行, + 结果整个项目 0 个缓存槽、`Home` 依旧未编译 —— 注解模式连原本能编的那 44 个都停了,纯亏。 + +### 反向验证(证明取证方法不是恒为真) + +在 `reactCompiler: true` 下,只加/删 `page.tsx` 文件顶部一行 `"use no memo"`,其余不动: + +- 加上:`frontend/src/app/page.tsx compiled functions: 0` +- 删掉:`frontend/src/app/page.tsx compiled functions: 2`(槽 11、24),全项目 44 个槽 + +两段有可见差异,且差异只落在 `page.tsx` 上(其他文件的槽数不变),说明这套取证能真实反映 +`page.tsx` 的编译状态,不是恒为真。同时它在两种状态下都判定 `Home` 未编译, +所以「`Home` 没被编译」不是取证方法的假象。反向验证用完已删除该行,`page.tsx` 与 origin/staging 逐字节一致。 + +### 结论与处置 + +任务 2 的验收(拿出 `Home` 被编译的证据)**没做成**,而且不是配置写错,是 Next 16.3.1 的 Rust 版 +React Compiler 在所有六种配置下都拒编 `Home`,包括显式 `"use memo"` opt-in。 +把它编译过去只剩两条路,两条都越界,所以都不做、写进 `BLOCKED.md`: + +1. 改 `page.tsx` 的业务代码去迎合编译器 —— 任务书明令禁止(「哪处代码挡住了编译器就写进 BLOCKED.md」)。 +2. 换 Babel 版编译器(`turbopackRustReactCompiler: false`)—— 需要新增依赖 + `babel-plugin-react-compiler`(next 的可选 peer,未装,Next 只内置了 runtime 没内置 plugin), + 撞「不新增依赖」。 + +按任务 2 止损条款「连败 3 次改注解模式;仍失败就回滚编译器配置、保留任务 1 的升级、如实汇报」, +已把 `next.config.ts` 回滚到与 origin/staging 逐字节一致(`git diff` 为空),保留 Next 16.3.1。 +用掉 6 轮验收,未满 12 轮;停下的原因不是轮数,而是不越界的配置空间已经穷尽。 + +## 任务 3 · 构建代价(四个数) + +体积口径与基线一致:汇总 `.next/server/app/index.html` 引用的全部 `/_next/static/**.js`, +去重后逐个 gzip(Node `zlib.gzipSync` 默认级别)再相加。`/` 是静态预渲染路由(构建输出标 `○ /`), +`index.html` 存在,共 21 个 JS chunk。测量脚本 `/tmp/measure_first_load.mjs`,不进仓库。 +每次构建前都 `rm -rf .next`(同时清掉 `.next/cache/turbopack`),四次都是冷构建。 + +四个数(任务书要求「各测 2 次取较快一次」,下表按前 2 次取最快给出): + +| | 编译器关闭 | 编译器开启 | 变化 | +| --- | --- | --- | --- | +| `next build` 耗时 | **41.29 s** | **35.11 s** | −6.18 s | +| `/` 首屏 JS(gzip 汇总) | **481397 B = 470.1 KB** | **493428 B = 481.9 KB** | +12031 B / **+2.50 %** | + +必须说清的一点:耗时这两个数不可信,同机噪声远大于效应。实际观测到的全部冷构建耗时是 +关闭 41.29 / 130.27 / 36.65 / 37.26 s,开启 100.43 / 35.11 / 39.21 s —— +机器安静时两边都落在 35~41 s,偶发的 100 s 与 130 s 是外部负载造成的离群值。 +所以正确结论不是「开启后构建更快」,而是**Rust 版编译器在本仓库没有可测量的构建变慢**, +这与官方「Rust 版为缓解构建变慢而做」的说法方向一致。为避免误导,我补测到每边 4 次才敢这么说。 +按任务书口径「取较快一次」,前两次的最快值就是上表的 41.29 与 35.11。 + +体积是确定性的:关闭态 4 次构建都是 481397 B,开启态 2 次都是 493428 B,逐字节可复现。 +首屏涨 2.50 %,低于 5 % 阈值,不触发任务书的强制说明条款。 + +保留还是回滚的建议:本轮**已按任务 2 止损条款回滚**。若纯看这三个数, +「构建不变慢、首屏 +2.5 %、换 44 个组件自动记忆化」本身是笔划算的交易, +而且那 44 个里的 `app-sidebar`(112 槽)、`sidebar-session-row`(73 槽)、`birth-date-picker`(58 槽)、 +三个引导 hook 都在 `/` 上渲染,收益落在同一个页面上。 +但本轮的目标是 `Home`,`Home` 一个槽都没拿到;那 44 个组件的实际收益本轮没有测量(没有渲染基准), +在「收益未测量 + 首屏确定变大 + 止损条款明确要求回滚」三者叠加下,我选择回滚, +把「是否为了这 44 个组件而保留 `reactCompiler: true`」作为一个独立决策交出去, +而不是顺手替决策者拍板。要重新开启只需在 `frontend/next.config.ts` 加回两行, +本文件的第 1/6 次尝试记录了确切写法。 + +## 收尾 + +`docs/BUG_HISTORY.md` 追加 `BUG-255`。按 BUG-253 的防复发要求先检索最大编号:本地基点 +`e8d201dd` 的最大编号是 253,但期间 `origin/staging` 已推进到 `fb9eec54` 并占用了 254 +(标题「22 条记录的正文挂在别人的标题下…」),所以顺延为 255 —— BUG-253 记录的抢号失效模式又发生了一次, +已在记录里注明。提交前 `npx tsc --noEmit` 无输出、`npx eslint` 0 error(4 个既有 warning)。 + +交付内容(相对 merge-base `e8d201dd`):`frontend/package.json`(next 一行)、 +`frontend/package-lock.json`(两笔:规范化重排 + 升级)、`docs/BUG_HISTORY.md`(+15 行)、 +`BLOCKED.md`(+10 行)、本文件(新建)。 +`frontend/next.config.ts` 已回滚、`frontend/src/**`、`frontend/tests/**`、`tests/**`、CI 配置零改动, +`git diff` 逐项为空。最终测试 tests 1592 / pass 1592 / fail 0 / cancelled 0 / skipped 0 / todo 0。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index e7329fad..6dca5b8d 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -3831,4 +3831,18 @@ - 待跟进:run 2(问题“请综合说明我当前最值得关注的主题”的那次 `calculation_failed`)不由本机制解释——该文本不含任何领域关键词,每个领域都会回落到自己的 `themes` 并对上,本轮未能找到独立证据说明它为何失败,不认领。同一问题文本的一次失败已在 BUG-257 记为领域数乘单领域耗时超出时钟预算,但本轮没有该次运行的领域数与耗时证据可核对,因此既不视为已解释也不视为复发。另记:领域循环里任一领域抛出即让整次工具调用失败,BUG-257 已有 `omitted_domains` 这条降级披露通道,把单领域失败也接入该通道属独立改动,本轮未做——本次修复消除的是那个确定性的失败源。 - 防复发:同一个决定必须只有一个权威来源。凡服务端自己签发的受控元数据已经声明了执行参数,就不得再由请求文本的启发式重新推导一遍并要求两者相等——这类“契约允许了服务端不接受的东西”的自伤矛盾在 fail-closed 门禁下必然表现为确定性 400。文本关键词只能作为没有声明时的兜底,且必须在返回值里标明本次由哪套规则决定。为了让文本路由同意声明路由而改写用户问题(如注入关键词前缀)不是修复而是绕行:它只覆盖被打补丁的那一个领域,还会污染模型据以作答的输入,一旦声明路由成为权威必须删除。凡是“一次运行对同一文本发出多次不同领域调用”的形态,测试必须带上产品真实发送的计划元数据并逐领域断言执行路由等于声明领域,否则测试会用一段刚好自洽的文本掩盖矛盾(本 bug 正是如此漏过)。 - 相关记录:BUG-257、BUG-256、BUG-255 + +## BUG-260 | React Compiler 无法接管 `page.tsx` 的 `Home`:编译器静默拒编 2730 行组件且不报任何错 + +- 状态:won't fix(本轮不修;Next 已升到 16.3.1 并保留,编译器配置已回滚,原因见下) +- 首次发现:2026-08-17 +- 最近更新:2026-08-17 +- 影响面:`/` 主对话页 `frontend/src/app/page.tsx` 的重渲染性能,以及后续任何「靠 React Compiler 免除手写记忆化」的计划。 +- 用户现象:无终端用户可见现象。对维护者而言的现象是:`next.config.ts` 里 `reactCompiler: true` 开启后构建打印 `✓ turbopackRustReactCompiler`、退出码 0、测试全绿,**看起来完全成功**,但 `Home` 一个函数都没被优化。这个失败不产生任何警告、错误或日志,只看构建输出无法察觉。 +- 根因:`page.tsx` 有 3738 行,其中 `export default function Home()` 单个函数占 2730 行(1002–3738),带 24 个 `useState`、18 个 `useEffect`、0 个手写 `useCallback`/`useMemo`。Next 16.3.1 的 Rust 版 React Compiler 会正常编译同一文件里的其他函数,却拒编 `Home`:默认 `infer` 模式下全项目 44 个函数拿到缓存槽(`app-sidebar` 112 槽、`use-birth-time-guided-journey` 84 槽、`sidebar-session-row` 73 槽等),`page.tsx` 内部两个小组件(原始行 760、815)也拿到了 11 和 24 槽,唯独 `Home` 的生成代码仍以裸 `useState` 序列开头、没有 `_c(N)` 前导。把 `compilationMode` 设为 `all`(绕过组件识别启发式、强制编译每个函数)后 `page.tsx` 被编译函数从 2 涨到 47,`Home` 依然不在其中——既然 `all` 模式下不存在「未被识别为组件」,只剩一种解释:编译器尝试了 `Home` 并失败,然后按 `panicThreshold` 默认值 `none`(官方文档原话 "skips components which cannot be compiled")静默跳过。具体是 `Home` 哪一处代码触发失败,本轮**未能定位**:`panicThreshold: "all_errors"` 在 Rust 版下形同虚设,设了也不报错、不打印任何编译器诊断。 +- 修复:未修复。`next.config.ts` 的 `reactCompiler: true` 与 `experimental.turbopackRustReactCompiler: true` 已回滚到与 `origin/staging` 逐字节一致;Next 16.2.10 → 16.3.1 的升级保留(该升级本身独立验证通过,是 Rust 版编译器的前置条件)。本轮共试六种配置全部失败:`reactCompiler: true`、加 `panicThreshold: "all_errors"`、再加文件顶部 `"use memo"`、`compilationMode: "all"`、`"all"` + `all_errors`、`compilationMode: "annotation"` + `Home` 体内 `"use memo"`。其中 `compilationMode: "all"` 还会直接把构建搞坏:它会编译模块作用域的普通回调,`birth-time-intake.tsx:22` 的 `Array.from({length:24}, (_, index) => ...)` 被插入 `useMemoCache`,预渲染 `/` 时抛 `TypeError: Cannot read properties of null (reading 'useMemoCache')`。注解模式(领导指定的退路)反而最差:全项目 0 个缓存槽,连原本能编的 44 个也停了。 +- 验证:`npx tsc --noEmit` 无输出;`npx eslint` 0 error(4 个既有 warning 在未改动文件里);`npx next build` 退出码 0;非数据库套件 199 个文件 1592 条全绿、fail 0 skipped 0 todo 0(与开工基线一致)。取证方式:`Home` 在产物里被压缩改名,`function Home(` 搜不到,改用 source map 反查——解 `.next/server/chunks/ssr/*.js.map` 的 VLQ mappings,把每个缓存槽 `_c(N)`(压缩后真实形态是 `(0,X.c)(N)`)归属回原始文件与行号。反向验证:只加/删 `page.tsx` 顶部一行 `"use no memo"`,`page.tsx` 被编译函数数在 2 与 0 之间可见切换,且切换只影响 `page.tsx`、其他文件槽数不变,证明取证方法不是恒为真;两种状态下都判定 `Home` 未编译。构建代价(各测 4 次取最快,同机噪声大):开启前 36.65s / 关闭后 35.11s,差异落在噪声内,Rust 版没有可测量的构建变慢;`/` 首屏 JS gzip 470.1 KB → 481.9 KB,+12031 B / +2.50%,低于 5% 阈值。 +- 待跟进:其一,定位 `Home` 里究竟哪一处挡住编译器,需要能出诊断的编译器;Rust 版不出诊断,Babel 版(`turbopackRustReactCompiler: false`)能出,但要新增依赖 `babel-plugin-react-compiler`(next 的可选 peer,未装;Next 只内置了 compiler runtime,没内置这个 plugin),需授权。其二,是否为了那 44 个能编的组件而保留 `reactCompiler: true`:它们大多在 `/` 上渲染(侧边栏、会话行、日期选择器、三个引导 hook),收益真实但本轮未测量,代价是首屏 +2.5%;本轮按止损条款选择回滚,这个取舍留给决策。其三,若要让 `Home` 真正受益,最可能的路是把它拆成若干个小组件——但那是业务代码重构,本轮明令禁止碰 `frontend/src/**`。 +- 防复发:**「配置开启 + 构建绿」绝不等于「React Compiler 生效」**。编译器放弃某个组件时不报错、不警告、不留日志,这是它的默认行为(`panicThreshold: "none"`)而非缺陷。任何启用 React Compiler 的改动都必须在构建产物里定位目标组件、确认存在编译器注入的缓存槽,并用 `"use no memo"` 做一次反向验证证明取证方法会随编译状态变化;只贴构建退出码等于没验证。另外 `"use memo"` / `"use no memo"` 是函数体内的指令,写在文件顶部时 `"use no memo"` 可作整文件退出、但 `"use memo"` 不构成整文件加入。 +- 相关记录:BUG-260 无前序同类记录。本记录三次改号:初次写作取 255;第一次 rebase 到 `origin/staging`(`c8d9ec64`)时远端已占 254–255,改 256;第二次 rebase 到 `e1db5762` 时远端又占到 259,改 260。每次都按 BUG-254 的防复发要求处理——标题与远端各记录均不同,故另分新号而非合并。BUG-253 所记的抢号失效模式在同一轮交付里连续复现两次,暴露出「追加前检索最大编号」这条措施的边界:它只在写记录那一刻成立,防不住推送前远端继续前进。可靠做法是把定号推迟到推送前最后一次 rebase 之后。 - 修复版本:本地未提交候选