5.6 KiB
PROGRESS · 次级页进入时的等待态与抖动(2026-09-18)
分支:codex/secondary-page-entry-20260918
基线:origin/staging @ 1061514f(任务书写的 41902067 是写单时的 head;本 worktree 按开工指令跟当前 origin/staging)
方案:产品 2026-09-18 选定方案一。方案 B 未做。
BUG-967 调查:Next 16.3.1 的 mismatch fallback 为什么没开火
对照 next@16.3.1 源码(GitHub tag v16.3.1),不是猜的。
Next 自己会在什么时候整页跳
packages/next/src/client/components/router-reducer/fetch-server-response.ts:
- RSC 响应
Content-Type不是text/x-component(export 模式下还接受text/plain),或!res.ok,或没有 body →doMpaNavigation。 - 解出 Flight 之后:
(res.headers.get(NEXT_NAV_DEPLOYMENT_ID_HEADER) ?? flightResponse.b) !== getNavigationBuildId()→ 同样 MPA。 app-router.tsx看到pushRef.mpaNavigation才location.assign/replace,并throw unresolvedThenable卡住当前树。
另有 nav-failure-handler.ts 的 handleHardNavError:导航过程中未捕获错误则 window.location.href = window.next.__pendingUrl。整份文件包在 process.env.__NEXT_APP_NAV_FAIL_HANDLING 里,standalone 默认不打开。
既有 StaleClientRecovery 只在 window.error / unhandledrejection 文本像 chunk 失败、且 sessionStorage 还没记过一次时 reload()。router reducer 如果把 rejection 吃掉,它看不见。
为什么这次三条都不触发
- 预取缓存让检查根本不跑。 侧栏三项是
<Link>。标签页在部署前已经打开对话页时,Next 已经把/chart/ephemeris/reports的 Flight 预取进内存。点击走这段缓存,不再fetchServerResponse。构建 id 比对、非 2xx、非 Flight Content-Type,全部不执行。 - 自托管没有 Vercel 那种
?dpl=分流。next.config.ts写了deploymentId: process.env.NEXT_DEPLOYMENT_ID,静态资源带?dpl=。在 Vercel 上这个 query 把请求打到对应部署,旧部署不在了就是 404,于是走条件 1。本仓 Caddy(deploy/Caddyfile.staging/Caddyfile.production.selfhosted)对/_next/static没有任何特殊 404 改写,只reverse_proxy web:3000。主机上只有当前镜像。旧 tab 就算真的去 fetch/chart,新服务器仍对这条路径返回 200 + 新的 Flight。条件 1 的!res.ok不成立。BUG-204 当年靠deploymentId修好的是 Vercel 形状,不是这台 VPS。 - 错误回退开关是关的。 旧 chunk 404 变成
ChunkLoadError时,默认 standalone 不会把它升级成 MPA。点击看起来像没反应。app-router.tsx若已经丢出unresolvedThenable或startTransition一直 pending,同一页上的账户按钮(本就不是 Link)也会一起没反应——这与「对话还能发」不矛盾:对话代码在内存里,路由切换被卡住。
Caddy 不是元凶:它没有把 404 变成 200 HTML。问题是 200 的新 Flight 加上 根本不再 fetch 的预取缓存。
自愈怎么做(才没有废掉正常客户端导航)
- 客户端 commit:构建期
NEXT_PUBLIC_GIT_COMMIT(Dockerfile 里与NEXT_DEPLOYMENT_ID同值写入,不改 workflow)。 - 服务端 commit:已有的
GET /api/health→.deployment.gitCommit(运行时GITHUB_SHA)。 StaleBuildGuard在挂载和visibilitychange → visible时拉一次 health。不轮询、不定时 reload。- 导航前读这份快照。不一致:
window.location.assign。一致:原样走Link/router.push。health 失败或任一侧是空/unknown:不强制跳。
BUG-966
三页改为 SecondaryPageShell。.secondary-page 是 chat-panel 第二行(minmax(0,1fr)),等待与正文同一格子;等待句居中。SecondaryHeader reserveNote 让出生行晚到时标题行不塌。模块级缓存 + 侧栏 pointerenter/pointerdown 预取。报告列表首屏去掉 InlineSpinner「正在读取报告…」,改静态句「报告列表还没拿到。」刷新按钮上的 spinner 仍是用户点的动作,DESIGN 允许。
未做方案 B(延迟揭幕)。
测试
| 命令 | 结果 |
|---|---|
npx tsc --noEmit |
0 错 |
npm run lint |
0 error(既有 120 warning,本轮文件未新增) |
定向:secondary-page-entry / chart-page-view / ephemeris-page / sidebar-contract / stale-client-recovery / personal-report-entry / chat-navigation-a11y-contract / class-name-definition-contract |
114 / 114 |
personal-report-view.test.ts |
通过(详情页仍用 SecondaryHeader) |
next build |
compile + tsc 过;收集 /api/daily-starlanguage 时 Windows EPERM 无法 symlink skill runtime。环境缺口,与本轮无关。/ Static 与 gzip 未在本机量到。 |
无 Chrome:高度真机清单在 docs/testing/secondary-page-entry-20260918.md。无登录态:缓存第二次进入与跨部署自愈的浏览器步骤同样写在那里。
改过的既有断言:
| 测试 | 原值 | 新值 | 原因 |
|---|---|---|---|
sidebar-contract 只读行 / 页脚 |
<Link className="session-main">、<Link className="profile-trigger" href="/"> |
AppLink,class 与 href 不变 |
BUG-967 跨部署自愈;只读行仍是链接、页脚仍去 / |
偏离
- 任务书写
app/(secondary)/layout.tsx,当前树是app/(app)/layout.tsx(会话列表单源之后)。外壳挂在这里,没有把路由组改回去。 - 报告详情
/reports/[reportId]仍用SecondaryHeader各阶段一份,不进三页进入抖动的范围;loading.tsx 里的 spinner 本轮不动。