pagehide 是唯一兼顾 bfcache 兼容性与状态存盘可靠性的原生事件,其核心在于 event.persisted 布尔值可区分页面是被缓存冻结还是真正卸载,需结合 visibilityState 验证并用 sendBeacon 安全落库。

pagehide 是目前唯一能兼顾 bfcache 兼容性与状态存盘可靠性的原生事件。直接用 unload 不仅会禁用缓存、导致 iOS/Android 返回白屏,还大概率发不出请求——它在 Safari 中连 fetch 都不等返回就销毁上下文。
为什么 pagehide 能替代 unload,但不能无脑替换
pagehide 的核心价值不在“它发生了”,而在 event.persisted 这个布尔值:它告诉你页面是“被冻结进缓存”还是“真要彻底消失”。unload 一触发,bfcache 就作废;而 pagehide + event.persisted === false 才是你该做最终落库的信号。
常见错误现象包括:
- 监听
pagehide后无条件调用saveState(),结果用户切 Tab 就写一次草稿,反复覆盖有效输入 - 在
event.persisted === true分支里调用fetch或setTimeout,这些操作在 iOS Safari 冻结阶段直接静默丢弃 - 完全忽略
visibilityState,导致 App 切后台(Safari 激进触发pagehide)时误判为“离开”
如何正确使用 event.persisted 判断真实退出场景
别只看 pagehide,要和 document.visibilityState 联合验证。Safari 在 App 进入后台时就发 pagehide,但此时 document.visibilityState 仍是 'visible' 或刚变 'hidden',不代表用户要关页。
实操建议:
- 只在
event.persisted === false且document.visibilityState === 'hidden'同时成立时,才执行关键保存逻辑 - 对高频触发场景(如快速切 Tab),可用
performance.now()记录时间戳,100ms 内重复触发直接忽略 - 避免在判断逻辑里写耗时同步代码(比如深 clone 大对象),防止阻塞冻结流程
必须用 navigator.sendBeacon 替代 fetch/XHR
fetch 和 XMLHttpRequest 在 pagehide 中不可靠:Chrome 可能中止未 resolve 的 Promise,iOS Safari 更是直接切断网络栈。只有 navigator.sendBeacon() 是浏览器明确保障“尽力发送”的机制,且不阻塞页面卸载。
注意点:
-
sendBeacon只支持POST,且 payload 必须是ArrayBuffer、Blob或FormData,不能直接传普通对象 - 数据量建议控制在 64KB 以内,超大会被 Chrome 截断或 Safari 拒绝
- 不要依赖它的返回值——它永远返回
true,成功与否无法得知,只能靠服务端幂等设计兜底
freeze 事件比 pagehide 更早、更确定
iOS Safari 和新版 Chrome 会在 pagehide 前先发 freeze,此时 JS 执行环境尚未冻结,是最后能安全调用 saveState() 的时机。错过它,pagehide 里可能只剩半截执行权。
推荐组合策略:
- 监听
freeze:立即序列化关键状态并标记frozen = true - 监听
pagehide:检查!frozen && !event.persisted,补一次保存(防 freeze 未触发的极少数情况) - 恢复时用
pageshow+event.persisted判断是否从缓存恢复,决定是否重载或复用已有状态
真正容易被忽略的不是语法,而是「冻结发生时你写的那行代码还在不在执行队列里」——queueMicrotask 在 freeze 前插入的任务大概率能跑完,但 setTimeout(..., 0) 几乎必丢。

















