diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 9a71f433..2007b7e4 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -3839,7 +3839,7 @@ - 最近更新: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")静默跳过。具体原因经授权后已定位(`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)逐一对上,两条独立证据互相印证。 +- 根因:本条所有行号与槽数均测于交付基点 `e8d201dd`;`page.tsx` 此后仍在演进(交付时远端已改到 3719 行),复现时应按函数名而非行号定位。`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` 的失败原因已获授权并完成,答案是上游编译器缺陷,不是本仓库代码问题。同时用同一套诊断量化了「换编译器版本能否提高覆盖率」这个问题,答案是**不能**:对 `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` 拆小,那样每个小组件天然落在编译器能处理的范围内,同时也解决可维护性问题。