Files
Jyotisha/PROGRESS-react-compiler-20260817.md
T
Jesse_Chen f78ad09fcc docs: close the open reactCompiler keep-or-roll-back decision
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>
2026-08-17 18:47:46 +08:00

212 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 CompilerRust 版)真正编译 `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` 这一个函数被拒」。
- 第 45 次把 `compilationMode` 设成 `all`(绕过组件识别启发式,逼编译器编译每个函数),
`page.tsx` 被编译函数从 2 涨到 47`Home` 仍然没有。既然 `all` 模式下不存在「没被识别成组件」
这回事,那就只剩一种解释:编译器尝试了 `Home` 并失败,然后按 `panicThreshold` 默认值
`none`(文档原话「skips components which cannot be compiled」)静默跳过。
- `panicThreshold: "all_errors"` 在 Rust 版下形同虚设:第 2、5 次都设了,构建既不报错也不打印
任何编译器诊断。所以拿不到「`Home` 到底哪一行挡住了编译器」的具体原因。
- 第 45 次的 `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`
去重后逐个 gzipNode `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 |
| `/` 首屏 JSgzip 汇总) | **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 error4 个既有 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。