f78ad09fcc
Decision is to stay rolled back: the round's target was Home, and the 44 functions the compiler does optimize have no measured render benefit to justify the +2.50% first-load JS. Reopening this should start by benchmarking those 44, not by flipping the config. Co-authored-by: Cursor <cursoragent@cursor.com>
212 lines
15 KiB
Markdown
212 lines
15 KiB
Markdown
# 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 次尝试记录了确切写法。
|
||
|
||
**该决策已有结论(2026-08-17,交付后确认):维持回滚。** 理由是本轮目标是 `Home`,
|
||
而那 44 个组件的运行时收益没有测量数据支撑首屏 +2.50 % 的代价。
|
||
若日后要改变这个结论,前置条件应当是先给那 44 个组件测出渲染基准,而不是直接开配置。
|
||
|
||
## 收尾
|
||
|
||
`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。
|