diff --git a/BLOCKED.md b/BLOCKED.md index 83906ff8..7038e56e 100644 --- a/BLOCKED.md +++ b/BLOCKED.md @@ -6,8 +6,9 @@ ## 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` 为空)。 +- **~~`Home` 的编译失败原因无法定位~~ 已解除并已定位(2026-08-17,用户授权新增依赖后完成)。** 结论:`Home` 的失败全部来自上游编译器未实现的语法与内部断言失败,**没有一条是本仓库代码写错**。稳定版 `babel-plugin-react-compiler@1.0.0` 报 24 条错误、三类全带 `Todo:`(编译器标记「尚未实现」):13 条 `try/finally`、9 条 `try/catch` 内的 `throw`、2 条 `MemberExpression cannot be safely reordered`。更新的 experimental 版已修好前两类,`Home` 随即撞上两个连续的编译器内部断言失败(`Invariant:`,即编译器 bug):`page.tsx:2666` 的 `catch (error)` 绑定被闭包捕获,以及 `page.tsx:1837` 的提升函数 `refreshAccount`。诊断用的依赖已卸载、锁文件已用 `npm ci` 权威还原、`git diff` 为空,不进交付——因为它买不到任何东西,见下一条。 +- **换编译器版本或换 Babel 版实现,对本仓库零增益(已量化,这条路可以关掉了)。** 对 `frontend/src` 全部 373 个文件跑批量诊断:稳定版 1.0.0 与 experimental 版**编译成功的函数数完全相同,都是 134 个**,失败文件都是同样 21 个,仅报错事件从 74 降到 48。原因是被上游修掉的错误类别,所在函数都还有别的拦路错误,于是一个函数都没多编译出来。所以别再指望「升个编译器版本就好了」。 +- **`Home` 要真正受益很可能得拆组件,属业务代码重构,本轮明令禁止。** 诊断结果进一步说明这条路该怎么走:为迎合编译器去改 `Home`(删 `finally`、重排 6 个被声明前引用的函数、逐个绕内部断言)是被编译器 bug 牵着走的打地鼠,每步都拿确定的正确性换不确定的收益;值得做的是按职责把 `Home` 拆小。 证据是同一文件里两个小组件(原始行 760、815)被正常编译、全项目另有 44 个函数被编译,唯独这个 2730 行、24 个 `useState`、18 个 `useEffect` 的函数被拒。任务书要求「哪处代码挡住了编译器就写进 BLOCKED.md,不要自行改写」,故 `frontend/src/**` 一行业务代码未动(仅反向验证时临时加删一行 `"use no memo"`,已还原,`git diff` 为空)。 - **~~待决策~~ 已决策(2026-08-17):维持回滚,不为那 44 个组件保留 `reactCompiler: true`;若要翻案,前置条件是先测出那 44 个的渲染基准。** 本轮按任务 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,按「不许顺手升别的依赖」未动。 diff --git a/PROGRESS-react-compiler-20260817.md b/PROGRESS-react-compiler-20260817.md index 3b1b48dc..567ce54d 100644 --- a/PROGRESS-react-compiler-20260817.md +++ b/PROGRESS-react-compiler-20260817.md @@ -197,6 +197,69 @@ React Compiler 在所有六种配置下都拒编 `Home`,包括显式 `"use mem 而那 44 个组件的运行时收益没有测量数据支撑首屏 +2.50 % 的代价。 若日后要改变这个结论,前置条件应当是先给那 44 个组件测出渲染基准,而不是直接开配置。 +## 追加轮 · 授权新增依赖后的根因诊断(2026-08-17) + +用户授权新增依赖,于是把 `BLOCKED.md` 第一条(拿不到编译器诊断)解掉。做法:临时 +`npm i -D babel-plugin-react-compiler`,用 `@babel/core` 的 `transformSync` 单独跑 `page.tsx` +(`parserOpts.plugins = ["jsx", ["typescript", {isTSX:true}]]`),给插件传 `logger` 钩子逐函数收事件。 +脚本在 `/tmp/rc_diagnose.mjs`、`/tmp/rc_batch.mjs`,不进仓库。 + +### `Home` 到底被什么挡住 + +稳定版 `1.0.0` 对 `page.tsx` 报 26 个事件:`CompileSuccess` 2、`CompileError` 24。 +`Home`(行 1002)的 24 条去重后三类,**全部带 `Todo:` 前缀** —— 这是 React Compiler +标记「该语法尚未实现」的前缀,不是用户代码违规: + +| 条数 | 原因 | +| --- | --- | +| 13 | `Todo: (BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause` | +| 9 | `Todo: (BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch` | +| 2 | `Todo: (BuildHIR::node.lowerReorderableExpression) Expression type MemberExpression cannot be safely reordered` | + +那 13 个 `finally` 做的全是 `finally` 该做的事:`window.clearTimeout(bootstrapTimeout)`、 +`polling = false`、`rectificationOpenInFlight.current = false`、 +`cancellationRequests.current.delete(requestId)`,以及 8 处 `setProfileSaving(false)`/ +`setAvatarSaving(false)`/`setCreatingSession(false)`/`setBirthTimeAssessmentPhase(null)` 这类加载态复位。 +删掉任何一个,try 块抛错时 UI 就永久卡在加载态 —— 那是拿真 bug 换假优化,所以一行没动。 + +顺带交叉验证了之前的产物取证:Babel 版报告编译成功的是 `BirthLocationFields @ 760` 与 +`ProfileFields @ 815`,和先前从 Rust 版构建产物 source map 反查出的槽 11/24(行 760/815) +逐一对上。两条完全独立的证据链互相印证,取证方法没问题。 + +### 更新版本能不能救 + +`experimental` 版(`0.0.0-experimental-a1856f3-20260507`)里那 22 条 `try/finally` 与 `throw` +错误已被上游修好,`CompileError` 从 24 降到 2。但 `Home` 立刻撞上编译器内部断言失败: +`Invariant: Expected all references to a variable to be consistently local or context references` +(`page.tsx:2666`,`catch (error)` 的绑定既被直接使用、又被 `setRequestError((current) => ...)` +的闭包捕获)。在仓库外的副本上把这一处改掉,又冒出下一个 +`Invariant: [PruneHoistedContexts] Unexpected hoisted function`(`page.tsx:1837` 的 `refreshAccount`, +被 1626 行的 `useEffect` 提前引用)。`Invariant:` 在 React Compiler 分类里是**编译器 bug**。 +`Home` 共 50 个函数声明,其中 6 个被声明前引用,要满足编译器得跨千行重排这 6 个定义。 +一句话:打地鼠,且每步都由编译器 bug 而非代码质量驱动。 + +### 换版本/换实现的全项目收益:零 + +对 `frontend/src` 全部 373 个文件跑批量诊断,两版对比: + +| | 稳定版 1.0.0 | experimental 版 | +| --- | --- | --- | +| 编译成功的函数数 | **134** | **134** | +| bailout 事件数 | 74 | 48 | +| 有 bailout 的文件数 | 21 | 21 | + +成功数一个没多。原因是被上游修掉的错误类别,所在函数都还有别的拦路错误。 +所以「升编译器版本」或「改用 Babel 版实现」这两条路由数据关闭, +`babel-plugin-react-compiler` 诊断完即卸载,锁文件用 `npm ci` 权威还原(`npm uninstall` 会留下 +3 条 `dev` → `devOptional` 的元数据漂移,已还原掉),`git diff` 为空,不进交付。 + +### 这一轮改变了什么结论 + +任务 2 的验收结论不变(`Home` 未被编译),但**原因的性质变了**: +从「不明原因的静默失败」变成「上游编译器缺陷,本仓库代码无错」。 +这直接影响后续该怎么做:不该去改 `page.tsx` 迎合编译器,该做的是按职责拆小 `Home`。 +交付物本身零变化 —— 配置仍是回滚态,`frontend/src/**` 仍是零改动。 + ## 收尾 `docs/BUG_HISTORY.md` 追加 `BUG-255`。按 BUG-253 的防复发要求先检索最大编号:本地基点 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 6dca5b8d..732d6938 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -3839,10 +3839,10 @@ - 最近更新: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 版下形同虚设,设了也不报错、不打印任何编译器诊断。 +- 根因:`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")静默跳过。具体原因经授权后已定位(`panicThreshold: "all_errors"` 在 Rust 版下形同虚设,设了也不报错,因此改用临时安装 `babel-plugin-react-compiler` 并挂 `logger` 钩子的方式取诊断,诊断完已卸载):**`Home` 的失败全部来自编译器自身未实现的语法与内部断言失败,没有一条是本仓库代码写错。** 稳定版 `babel-plugin-react-compiler@1.0.0` 对 `Home` 报 24 条错误,去重后三类,全部带 `Todo:` 前缀(React Compiler 用 `Todo:` 标记「该语法尚未实现」):13 条 `Todo: (BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause`、9 条 `Todo: (BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch`、2 条 `Todo: (BuildHIR::node.lowerReorderableExpression) Expression type MemberExpression cannot be safely reordered`。即编译器当时还不支持 `try/finally` 和 `try/catch` 内的 `throw`,而 `Home` 有 13 个 `finally` 和 9 个这样的 `throw`。这 13 个 `finally` 做的全是 `finally` 该做的事——`window.clearTimeout(bootstrapTimeout)`、`polling = false`、`rectificationOpenInFlight.current = false`、`cancellationRequests.current.delete(requestId)`,以及 8 处 `setProfileSaving(false)` / `setAvatarSaving(false)` / `setCreatingSession(false)` / `setBirthTimeAssessmentPhase(null)` 之类的加载态复位;删掉任何一个,try 块抛错时 UI 就永久卡在加载态,是拿真 bug 换假优化。另在更新的 `0.0.0-experimental-a1856f3-20260507` 上复测:那 22 条 `try/finally` 与 `throw` 错误已被上游修好,但 `Home` 随即撞上编译器内部断言失败 `Invariant: Expected all references to a variable to be consistently local or context references`(`page.tsx:2666`,`catch (error)` 的绑定同时被直接使用和被 `setRequestError((current) => ...)` 的闭包捕获);在仓库外的副本上把这一处改掉后,又冒出下一个 `Invariant: [PruneHoistedContexts] Unexpected hoisted function`(`page.tsx:1837` 的 `refreshAccount`,被 1626 行的 `useEffect` 提前引用)。`Invariant:` 在 React Compiler 的分类里是编译器 bug 而非用户代码违规。`Home` 共 50 个函数声明,其中 6 个被声明前引用(`refreshAccount` 1837/1626、`editDeclaredBirthTimeDetails` 2280/1098、`completeGuidedBirthTime` 2308/1097、`openRectificationFromHomepage` 2515/2373、`openRectificationSession` 2519/1982、`handleRectificationProfileIncomplete` 2531/2457),要满足编译器就得在 2700 行的组件里跨千行重排这 6 个定义,且照上述规律修完还会有下一个内部 panic。诊断顺带交叉验证了产物取证的正确性:Babel 版报告成功编译的两个函数是 `BirthLocationFields @ 760`、`ProfileFields @ 815`,与先前从 Rust 版构建产物 source map 反查出的两个缓存槽(行 760/815,槽 11/24)逐一对上,两条独立证据互相印证。 - 修复:未修复。`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/**`。 +- 待跟进:其一(已完成,结论见根因):定位 `Home` 的失败原因已获授权并完成,答案是上游编译器缺陷,不是本仓库代码问题。同时用同一套诊断量化了「换编译器版本能否提高覆盖率」这个问题,答案是**不能**:对 `frontend/src` 全部 373 个文件跑批量诊断,稳定版 1.0.0 与 experimental 版**编译成功的函数数完全相同,都是 134 个**,失败文件也都是同样的 21 个,只是报错事件从 74 降到 48——被上游修掉的那些错误类别,所在函数都还有别的拦路错误,所以一个函数都没多编译出来。因此换 Babel 版或换更新版本都买不到任何东西,`babel-plugin-react-compiler` 诊断完即卸载(`npm ci` 从锁文件权威还原,`git diff` 为空),不进交付。复现诊断的方法:临时 `npm i -D babel-plugin-react-compiler`,用 `@babel/core` 的 `transformSync` 配 `parserOpts.plugins = ["jsx", ["typescript", {isTSX:true}]]` 单独跑该文件,给插件传 `logger: { logEvent(file, event) {} }`,`event.kind` 为 `CompileError` 的即为 bailout,`event.detail.message` 是原因。其二,是否为了那 44 个能编的组件而保留 `reactCompiler: true`:它们大多在 `/` 上渲染(侧边栏、会话行、日期选择器、三个引导 hook),收益真实但本轮未测量,代价是首屏 +2.5%;本轮按止损条款选择回滚,这个取舍留给决策。其三,若要让 `Home` 真正受益,最可能的路是把它拆成若干个小组件——但那是业务代码重构,本轮明令禁止碰 `frontend/src/**`。诊断结果还把这条路的性质说清了:为迎合编译器去改 `Home`(删 `finally`、重排 6 个提升函数、逐个绕内部断言)是被编译器 bug 牵着走的打地鼠,每一步都拿确定的正确性换不确定的记忆化收益,不该做;真正值得做的是按职责把 `Home` 拆小,那样每个小组件天然落在编译器能处理的范围内,同时也解决可维护性问题。 - 防复发:**「配置开启 + 构建绿」绝不等于「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 之后。 - 修复版本:本地未提交候选