区分“被动隐藏”和“系统回收”需依赖freeze与pageshow(persisted===false)事件:freeze标志即将冻结,应同步保存状态;pageshow且persisted===false标志已被丢弃,需冷启动补写;pagehide+persisted===true为兜底路径;混合开发须结合原生层同步。

区分“被动隐藏”和“系统回收”不能靠肉眼判断,也不能只看 visibilitychange——它只说明页面不可见,不说明是否还活着。真正关键的是抓住浏览器给你的两个明确信号:freeze 和 pageshow 且 persisted === false。前者代表页面即将被冻结(还来得及保存),后者代表它已被丢弃、正在冷启动重建(该补写了)。
用 freeze 事件识别“即将冻结”,立刻做轻量固化
freeze 是页面进入 Frozen 阶段前的最后一个可执行 JS 的时机。此时 DOM 可读、定时器未停摆、fetch 还能 abort,但之后所有异步任务都会挂起。错过它,就等于放弃主动保存机会。
- 必须用
addEventListener('freeze', handler, { once: true }),避免重复绑定 - 在此回调中同步保存关键业务状态:表单输入值、播放进度、滚动位置、选中项 ID 等原始数据
- 只存可序列化内容:字符串、数字、布尔、纯对象、数组;剔除 DOM 节点、函数、EventTarget 实例
- 优先使用
localStorage.setItem()或sessionStorage.setItem(),避免 await 异步写入(可能被中断)
用 pageshow + persisted === false 识别“已被丢弃”,触发补写逻辑
当用户从后台切回一个被 Discarded 的页面时,浏览器会重新加载整个页面(全新 navigation),此时 pageshow 事件触发,且 event.persisted === false。这不是“没缓存”,而是“已重建”——你要把它当作一次冷启动来处理。
- 在入口脚本最顶部检查
event.persisted === false和document.wasDiscarded === true(后者仅首次 JS 执行时有效) - 结合 localStorage 中记录的最后保存时间戳:若距当前 > 3 分钟,基本可判定为 discard 后重建
- 补写动作包括:从本地存储恢复表单、重置播放器到上次进度、滚动到历史位置、重新拉取过期数据(如 token、订单状态)
- 不要直接调用
location.reload()或重复初始化全局模块,防止状态错乱
补漏:pagehide + persisted === true 是 Safari 和部分安卓 Chrome 的兜底路径
有些浏览器(尤其是 iOS Safari)不触发 freeze,而是走 pagehide 并把 event.persisted 设为 true,表示页面将被放入 bfcache。如果你只监听 freeze,这部分用户的状态就丢了。
- 在
pagehide中加一层判断:event.persisted === true && !frozenFlag,再执行一次 saveState() - 用
performance.now()记录上次保存时间,200ms 内不重复写入,防 freeze 和 pagehide 连续触发 - 避免在 pagehide 里读取 DOM 尺寸或 offset,此时元素可能已不可计算
不依赖前端的兜底方案:原生层状态同步(混合开发必做)
Page Lifecycle API 在国产安卓 ROM(华为、小米、OPPO)上常被阉割——freeze 不触发、pagehide 延迟严重。纯 Web 方案不可靠,必须引入原生协同:
- WebView 场景下,每次关键操作(如输入、拖动、点击提交)都通过 JSBridge 同步调用原生接口,把状态写入 SharedPreferences 或 Room
- Android Activity 的
onPause()中强制触发一次全量快照,比等页面事件更及时 - 对高价值状态(如订单草稿、音视频断点),启用前台服务争取 1–3 分钟保活窗口,完成异步落盘
- 用 WorkManager 注册兜底任务:即使进程被杀,也能在下次空闲时拉起独立进程完成最终持久化

















