sendbeacon并非绝对可靠,失败主因是浏览器仅“尽力发送”而非保证送达,且在ios safari、内存紧张或调用过晚时易被丢弃;必须提前准备数据、尽早于visibilitychange/pagehide触发,并严格控制payload大小与格式。

sendBeacon 为什么在页面卸载时仍可能失败
不是所有浏览器都真正保证 navigator.sendBeacon “一定成功”——它只是尽力而为:底层使用的是异步、无响应等待的 POST 请求,且浏览器只承诺“尝试发送”,不承诺送达。尤其在 iOS Safari、部分 Android WebView 或极端内存紧张场景下,请求可能被直接丢弃。更关键的是,如果调用太晚(比如在 beforeunload 里才构造数据),浏览器可能已冻结 JS 执行上下文,导致 sendBeacon 调用本身被跳过。
必须在 beforeunload 之前就准备好 beacon 数据并预发一次
可靠性的核心在于“提前准备 + 尽早触发”。不要等到用户真的关闭标签页才开始序列化数据、拼接 URL、调用 sendBeacon。实际做法是:
- 在用户关键交互(如点击、滚动、播放完成)后,立即更新一个全局缓存对象(如
window.__lastBeaconPayload),保持最新埋点快照 - 在
visibilitychange事件监听中,一旦检测到document.visibilityState === 'hidden'(页面失焦/切后台),立刻调用navigator.sendBeacon发送当前缓存数据——此时 JS 仍完全可执行,网络栈也未冻结 - 再额外在
pagehide事件中补发一次(注意:不是beforeunload,因后者在部分浏览器中不可靠或已被废弃)
示例关键逻辑:
let lastPayload = { event: 'page_exit', ts: Date.now() };
const sendExitBeacon = () => {
if (!navigator.sendBeacon) return;
const blob = new Blob([JSON.stringify(lastPayload)], { type: 'application/json' });
navigator.sendBeacon('/api/track', blob);
};
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') sendExitBeacon();
});
// pagehide 是更可靠的卸载前钩子,比 beforeunload 更早且兼容性更好
window.addEventListener('pagehide', () => {
if (!event.persisted) sendExitBeacon(); // 忽略 bfcache 场景
});
payload 大小和格式限制直接影响是否被静默丢弃
navigator.sendBeacon 对 payload 有隐式限制:Chrome 通常允许约 64KB,Safari 更保守(实测常低于 8KB),超出则调用直接返回 false 且无错误提示。同时,服务端必须能接收 Content-Type: text/plain 或 application/octet-stream 类型的原始 body(不能依赖 application/json 的自动解析)。
- 务必在调用前检查
navigator.sendBeacon(url, data)返回值,false表示浏览器拒绝发送(常见于超限或跨域) - 避免传入 JSON 字符串;改用
Blob或Uint8Array,显式指定类型,减少编码歧义 - 压缩关键字段:用短 key(如
t代替timestamp)、剔除非必要字段、服务端做映射还原 - 不要依赖
sendBeacon的响应——它本就不提供回调,也不支持设置 timeout
服务端必须接受无 CORS、无 cookie、无重试的单次裸请求
浏览器在页面卸载阶段会禁用所有非关键网络行为:Credentials 默认为 'omit',CORS 预检被跳过,HTTP/2 流复用可能中断。这意味着:
- 后端接口必须允许
Access-Control-Allow-Origin: *(或精确匹配源),且不能要求withCredentials - 不能依赖 session cookie 或 bearer token——所有认证信息需内嵌在 payload 里(如加密的
uid和短期sig) - 要容忍重复请求:用户快速刷新或双击关闭可能导致多次
pagehide触发,服务端需幂等去重(例如用event_id + timestamp组合判重) - 日志里若发现大量 4xx/5xx 但前端没报错,优先查 payload 解析逻辑是否崩了——因为失败不会抛异常,也不会进
catch
最易被忽略的一点:iOS Safari 在 PWA 模式下,从主屏幕启动的页面,pagehide 可能不触发,得靠 visibilitychange + 定时器兜底(例如失焦 3s 后强制发 Beacon)。










