navigator.sendBeacon在页面卸载时可能丢数据的根本原因不是API不可靠,而是调用时机不当:若在beforeunload中才拼接数据、序列化或读取localStorage,同步阻塞会挤占发送窗口;传字符串而非Blob等合规类型会静默失败;超64KB或强制关闭进程也会导致丢失。

为什么 navigator.sendBeacon 在页面卸载时仍可能丢数据
根本原因不是 API 不可靠,而是调用时机不对。浏览器只保证在 beforeunload 或 pagehide 事件中发起的 sendBeacon 请求会被排队发送,但不保证它一定发出——如果用户强制关闭标签页、杀进程、或系统休眠,请求可能被直接截断。更常见的是:你在 beforeunload 里才开始拼接数据、计算耗时、读取 localStorage,这些同步操作一旦卡住(比如遇到大 JSON 序列化或磁盘 I/O 延迟),就会挤占 beacon 发送窗口,导致失败。
-
sendBeacon只接受ArrayBuffer、Blob、FormData、URLSearchParams四种类型,传JSON.stringify(obj)字符串会静默失败(无报错,但服务端收不到) - 必须用
POST方法,且 Content-Type 被固定为text/plain(除非用Blob自定义类型) - Chrome 对单个 beacon 的 payload 大小限制约 64KB,超限直接拒绝,也不报错
如何在 React/Vue 单页应用中提前准备 beacon 数据
关键思路是「卸载时不生成,只转发」。所有埋点数据应在触发时就序列化并暂存,而不是等到页面要关了才收集。例如用户点击按钮、路由跳转、接口响应完成等时刻,立刻把结构化数据 push 到一个全局队列,并同步写入 localStorage(带时间戳和唯一 ID,防重复)。
- 用
new Blob([JSON.stringify(data)], {type: 'application/json'})构造 payload,避免字符串直传失效 - 每次埋点后检查队列长度,超过 20 条就主动触发一次
sendBeacon清空部分(别等卸载) - 在
visibilitychange事件中监听document.visibilityState === 'hidden',此时也应尝试 flush 队列——很多用户只是切走标签页,不是关页面
如何安全绑定到页面卸载生命周期
不要只依赖 beforeunload。它在现代浏览器中已被限制(不能弹窗、执行时间极短),且在 iOS Safari 和部分安卓 WebView 中完全不触发。优先使用 pagehide,再降级到 visibilitychange,最后兜底 unload(仅作日志记录,不指望它发成功)。
-
pagehide是最可靠的卸载钩子,即使用户从 PWA 添加到桌面,它依然有效 - 注册监听必须用
{once: true},防止多次绑定导致重复发送 - 在事件回调里立即调用
navigator.sendBeacon(url, blob),之后不要再做任何异步操作(比如await或setTimeout) - 服务端需容忍重复请求(靠埋点 ID 幂等去重),因为
pagehide+ 强制关闭可能造成两次触发(一次正常,一次系统级终止前的最后尝试)
怎样验证 beacon 是否真正发出
浏览器开发者工具的 Network 面板默认不显示 beacon 请求(它们标记为 beacon 类型,且不显示响应)。必须手动过滤:Filter → "beacon",或在控制台运行 performance.getEntriesByType('navigation') 查看 transferSize 是否 > 0。
- 本地测试时,可在服务端加日志:收到请求后立刻写入文件,再模拟 kill 进程,观察是否残留未写入记录
- 线上可对 beacon 接口加采样上报(如每 100 次返回一个
X-Beacon-Idheader),前端用performance.getEntriesByName()捕获该请求,反向验证发送行为 - 注意:Safari 对
sendBeacon的跨域策略更严,若用相对路径/log,确保与当前页面同源;若用绝对 URL,协议、域名、端口必须完全一致
实际落地最难的不是调用 API,而是让整个埋点链路放弃「最后时刻汇总」的思维——所有计算、序列化、校验必须前置,卸载钩子里只剩一条语句:navigator.sendBeacon(url, blob)。多一步都可能失败。










