BFCache 生命周期恢复:文档代际、陈旧异步结果与副作用对账
后退恢复的页面不是一次普通 reload,JS 堆和旧状态可能原样返回。本文用 pagehide/pageshow 合同、恢复代际和服务端 operation receipt 阻止重复提交与陈旧权限继续使用。
本文要点
- 主题聚焦:TypeScript、DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
浏览器后退时可能从 back/forward cache 恢复整个 Document 和 JavaScript 堆,而不是重新执行一次冷启动。旧变量、DOM 和未完成意图仍在,服务器上的权限、订单或 Feature Flag 却已经变化。把 pageshow 当 load 再跑一次提交,会制造重复写;什么都不做,又会让过期账号继续显示敏感内容。Web 性能 RUM提醒不要依赖 unload,本篇定义恢复时的正确性合同。
Document 身份跨恢复不变,恢复代际要前进
发布 pageLifecycle.v1,固定 app release、route contract、document instance ID 生成规则、可恢复状态清单、必须关闭的资源、恢复验证项、最大陈旧窗口和 fallback。一次 Document 的 documentInstanceId 在 BFCache 往返中不变;每次 pageshow 且 event.persisted === true 时单调增加 resumeGeneration。硬 reload 或新导航创建新 document ID。
ACTIVE -> PAGEHIDE_PENDING -> SUSPENDED_CANDIDATE
-> RESTORING -> ACTIVE | RELOAD_REQUIRED
pagehide.persisted 表示浏览器准备保留页面,不是持久化提交凭据。处理器停止领取新工作、保存纯本地 UI checkpoint,并关闭会妨碍缓存资格或已无法安全续用的 WebSocket、锁和观察器。关键业务写不能靠 sendBeacon 在离开时“最后赌一次”;beacon 只适合已允许丢失或服务端幂等的遥测。
不要添加永久 unload handler 来模拟生命周期。是否真正进入 BFCache 由浏览器决定;正常销毁、崩溃和内存压力仍可能让页面消失。需要跨销毁恢复的事实必须在服务端、已提交本地存储或用户导出中,而不是只放 JS heap。Local-first AI 应用解释本地优先取舍,这里不把 BFCache 当数据库。
恢复先撤销旧结果资格,再重连
进入 RESTORING 的第一步是关闭 ACTIVE gate 并提高 generation,让 pagehide 前创建的 fetch、timer、订阅 callback 和 worker message 即使晚到,也不能更新当前 DOM 或发起下一阶段动作。随后并行重验 session/auth epoch、route resource version、Feature Flag、CSRF/提交能力、Service Worker controller release 和 server operation 状态;任何关键验证未知都保持只读遮罩或转 RELOAD_REQUIRED。
验证完成后生成 resumeReceipt.v1,绑定 document instance、resume generation、auth/session epoch、route version、flag/policy generation、controller release、operation reconciliation root、验证结果摘要和短有效期。唯一恢复接受点是 activateResume 以 CAS 确认仍是同一 document/generation、状态仍为 RESTORING 且全部依赖版本未变,再保存 receipt 并推进 ACTIVE。新的 pageshow、controllerchange、登出/撤权通知或资源失效会关闭 gate、废弃整轮结果并提升 generation,不能只重跑其中一个检查。
所有 DOM commit 和后续 I/O 都同时携带 callback 创建时的 generation 与 active receipt ID;即使 callback 是恢复后新建,只要 gate 尚未打开、receipt 已过期或 ID 不再是当前值,就只能丢弃或重新验证。服务端仍对每次敏感请求执行实时授权和资源前置条件,receipt 只是页面恢复资格,不是绕过后续撤权的长期令牌。
实时连接用新 generation 和服务器 resume cursor 重建,不能复用旧 socket “看起来还开着”的内存对象。跨标签页 leader 必须重新竞争 Web Locks Leader 围栏,旧 owner 身份不能随堆恢复。Service Worker controller 若在暂停期间变化,页面按Service Worker 发布激活的 client protocol 重新握手。
用户明确提交一次写操作时,页面先获得稳定 operationId 和 payload digest,服务端原子接受;页面只缓存 receipt 引用。恢复后查询原 operation:SUCCEEDED 展示既有结果,PENDING/UNKNOWN 继续对账,只有权威 NOT_ACCEPTED 才允许原键重试。同一个按钮看似仍可点击,也不能生成第二个 operation。HTTP 幂等键正确性负责服务端效果绑定,本生命周期只管理 UI 是否有资格发起或展示。
回放真正的后退、冻结和权限变化
自动化在 Chromium、Firefox 和 WebKit 的受支持矩阵中执行 A -> B -> back,记录 pageshow.persisted;同时注入恢复前撤权、验证中再次撤权或 controllerchange、连续两次 pageshow、ACTIVE gate 打开前创建新 callback、旧 fetch 晚到、socket 半开、多标签 leader 转移、写成功但页面未记 operation receipt、内存压力导致非 BFCache reload,以及表单含未保存内容时强制 fallback。断言旧 generation 或非当前 resume receipt 的 DOM commit/I/O 为 0,恢复重复业务效果为 0,撤权内容可见窗口不超过策略 SLO。
门禁要求恢复路径关键 revalidation 覆盖率 100%、无 active resume receipt 的 DOM/I/O 接受数为 0、旧 callback 接受数为 0、验证期间依赖变更后旧 receipt 接受数为 0、同 operation 重复效果为 0、恢复交互 p95 低于 500 ms、BFCache 恢复失败都有 RELOAD_REQUIRED 原因。命中率只按浏览器能力分桶观察,不设一个强迫所有页面可缓存的总目标。先只采集 pagehide/pageshow receipt,再为只读路由恢复连接,最后开放带 operation 对账的表单。事故时可让已恢复页面立即受控 reload,牺牲速度但保留服务端幂等与未保存 checkpoint;不能通过加 unload 或清空状态掩盖竞态。
Related