docs(bug-history): pin BUG-260's measurements to the base they were taken on
Independent Staging Quality Gate / validate (push) Successful in 11m58s
Independent Staging Quality Gate / publish (push) Successful in 14m29s

page.tsx changed on staging while this batch waited to land, so the line
numbers in the record no longer resolve. State the base explicitly and tell
future readers to locate Home by name rather than by line.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-17 18:48:23 +08:00
parent 0cdc931790
commit 561010f2e7
+1 -1
View File
@@ -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 行(10023738),带 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 行(10023738),带 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 error4 个既有 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` 拆小,那样每个小组件天然落在编译器能处理的范围内,同时也解决可维护性问题。