页面卸载时如何可靠上报?sendBeacon 和 keepalive 有什么区别?
简化版
页面卸载时普通异步请求容易被浏览器取消,统计和埋点常用 navigator.sendBeacon() 或 fetch(..., { keepalive: true })。sendBeacon 适合少量 POST 上报且不关心响应;keepalive 更像 Fetch 语义,但也有请求体大小和生命周期限制。
详细版
典型用法:
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
navigator.sendBeacon('/log', JSON.stringify({ duration: 120 }))
}
})
相比 beforeunload 中强行同步请求,visibilitychange 更适合作为页面进入后台的信号。sendBeacon 会让浏览器在不阻塞页面卸载的情况下尝试发送数据;它不能读取响应,也不适合大 payload 或关键业务提交。
完整版教学
一、为什么卸载上报容易丢
用户关闭标签、跳转页面、刷新或移动端切后台时,页面的 JS 执行环境很快会被冻结或销毁。普通 fetch 还没发完,浏览器可能直接取消。
click link
-> 页面开始导航
-> 普通异步 fetch 还在 pending
-> 旧页面上下文销毁
-> 请求可能被中断
记忆钩子:卸载上报的目标不是“等响应”,而是“别挡用户离开,还尽量把小数据送出去”。
二、sendBeacon 的定位
navigator.sendBeacon('/analytics', new Blob([
JSON.stringify({ page: '/home', stay: 35 }),
], { type: 'application/json' }))
sendBeacon 专门用于少量遥测数据上报。它返回布尔值表示浏览器是否成功把数据排队,不表示服务端已经成功处理。它通常使用 POST,不提供读取响应体的能力。
这正好符合埋点需求:页面停留 35 秒、点击了 3 次按钮、即将离开时上报,不应该为了等日志响应阻塞跳转。
三、为什么推荐 visibilitychange
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
flushLogs()
}
})
unload 和 beforeunload 在现代浏览器中限制越来越多,还可能影响 bfcache。visibilitychange 在页面变为隐藏时触发,覆盖切后台、切标签、导航等更多实际场景。
如果用户只是切到另一个 App,页面未必立即卸载,但已经是一个很好的上报时机。移动端尤其要重视这个信号。
四、fetch keepalive 的特点
fetch('/log', {
method: 'POST',
body: JSON.stringify({ event: 'leave' }),
headers: { 'Content-Type': 'application/json' },
keepalive: true,
})
keepalive 允许请求在页面卸载后继续一小段时间。它保留了 Fetch 的写法,可以设置方法、头部等,但浏览器会限制请求体大小和数量,不能当成普通后台任务。
| 能力 | sendBeacon | fetch keepalive |
|---|---|---|
| 读取响应 | 不支持 | 语义上是 Fetch,但卸载后不应依赖响应 |
| 适合场景 | 埋点、统计 | 小型上报、需要自定义头时 |
| 阻塞卸载 | 不阻塞 | 不应阻塞 |
| 大文件上传 | 不适合 | 不适合 |
五、请求大小和可靠性边界
浏览器会对 beacon/keepalive 的待发送数据设置限制,常见经验是只放几十 KB 以内的小数据。假设一次日志 2KB,一次性上报 10 条约 20KB,一般合理;如果把 2MB 错误上下文塞进去,就不适合。
这些 API 提供“更可靠的尽力发送”,不是事务保证。网络断开、进程被杀、浏览器策略限制都可能导致失败。关键业务例如支付、下单、保存表单,不能依赖卸载上报完成。
六、工程实践
更稳的做法是平时分批上报,卸载时只 flush 少量剩余数据。埋点队列可以按数量、时间、页面隐藏事件触发发送。
用户操作 -> 进入队列
每 5 秒或满 20 条 -> 批量上报
visibility hidden -> flush 剩余小批量
同时要做脱敏和限流,避免把 token、手机号、完整表单内容等敏感数据塞进日志。日志上报不是安全豁免区。
七、常见误区与追问
- 误区:在
beforeunload里 await fetch 就可靠。 页面卸载不会等你的异步任务完成。 - 误区:sendBeacon 返回 true 表示服务端收到。 它只表示浏览器接受排队请求。
- 误区:卸载上报适合关键业务。 它适合统计日志,不适合支付、保存等强一致操作。
- 追问:为什么用
visibilitychange? 它覆盖页面隐藏和移动端切后台,且比 unload 更友好。 - 追问:keepalive 能传大文件吗? 不能,浏览器会限制大小和生命周期。
- 追问:如何提升可靠性? 平时批量上报,隐藏时 flush 小队列,服务端去重。
八、加强记忆
页面卸载上报要记“早发、小发、别等回包”:尽量在平时批量发送,不把所有日志压到离开瞬间;单次 payload 保持小;离开时用 sendBeacon 或 keepalive 尽力发送,不依赖响应。它解决的是埋点可靠性,不是关键业务提交。