sendbeacon在页面卸载时“看似”不触发的根本原因是浏览器对调用时机的严格限制:仅允许在pagehide或visibilitychange中快速执行,超时(≤100ms)或同步阻塞即丢弃;它不返回promise、不抛异常、不发送自定义请求头,且数据体不能超64kb。

sendBeacon 为什么在页面卸载时「看似」不触发
根本原因不是 API 失效,而是调用时机被浏览器拦截:如果在 beforeunload 或 pagehide 中发起非 sendBeacon 请求(比如 fetch 或 XMLHttpRequest),浏览器会直接中止;而即使用了 sendBeacon,若在事件处理函数里做了同步阻塞(如 alert()、长时间循环、或等待未 resolve 的 Promise),也会导致 Beacon 被丢弃——浏览器只给极短的异步执行窗口(通常 ≤ 100ms),超时即放弃。
实操建议:
- 必须在
visibilitychange(状态变为hidden)或pagehide事件中调用,beforeunload仅作兜底(部分浏览器已限制其行为) - 调用前确保数据已序列化完成,避免在回调里做
JSON.stringify等耗时操作 - 不要 await 任何 Promise,不要 try/catch 包裹 sendBeacon 调用(它本身不返回 Promise,也不抛异常)
- 检查目标 URL 是否同源,跨域需服务端配置
Access-Control-Allow-Origin: *且允许Content-Type: text/plain
sendBeacon 数据格式与后端接收的兼容性陷阱
sendBeacon 只支持 ArrayBuffer、Blob、FormData 和字符串(USVString),不支持直接传 Object。常见错误是传 JSON.stringify(data) 后没设 Content-Type,导致后端解析失败——但注意:sendBeacon **不发送请求头**,Content-Type 是由浏览器根据数据类型自动设置的:字符串 → text/plain,FormData → multipart/form-data,Blob → 由构造时指定的 type 决定。
实操建议:
- 优先用
URLSearchParams构造字符串:new URLSearchParams(data).toString(),服务端按 query string 解析最稳 - 若必须传 JSON,转成字符串后显式包装为
Blob:new Blob([jsonStr], {type: 'application/json'}),否则后端收到的是纯文本,MIME 类型却是text/plain - 避免用
FormData:它在某些旧版 Safari 中触发 Beacon 失败率高,且服务端需额外处理 multipart 边界 - 测试时用
curl -v -X POST -H "Content-Type: text/plain" --data-binary "@data.txt" http://your-endpoint模拟原始 Beacon 请求
如何验证 sendBeacon 是否真正发出并送达
浏览器开发者工具的 Network 面板默认不显示 sendBeacon 请求(因为它们在页面上下文销毁后异步发出),所以看不到不代表没发。真实链路是:JS 调用 → 浏览器排队 → 页面卸载 → 浏览器在后台线程发出请求 → 返回结果被丢弃(API 不提供成功/失败回调)。
实操建议:
- 服务端必须记录所有到达的 Beacon 请求(哪怕空 body),用于比对客户端日志
- 客户端在调用
sendBeacon前,把时间戳、随机 ID、页面 URL 存入localStorage;服务端响应后,前端可通过navigator.sendBeacon的返回值(布尔值)粗略判断是否进入发送队列(true表示已入队,false表示被拒,如 URL 无效或数据过大) - Chrome 117+ 可在
chrome://net-internals/#events中筛选REQUEST_ALIVE和SEND_HEADERS事件,搜索你的 Beacon URL - 数据体别超过 64KB:超出部分会被浏览器截断,且无提示
替代方案:当 sendBeacon 不可用时怎么保底
不是所有场景都可靠:iOS Safari 对 background task 限制极严,PWA 在 iOS 上可能完全无法触发 Beacon;WebView(尤其安卓低版本)也存在兼容问题。这时不能只依赖 Beacon。
实操建议:
- 页面可见时主动上报(如用户停留 ≥ 5s、滚动到底部、点击关键按钮),减少对卸载时刻的依赖
- 结合
visibilitychange+ 定时缓存:每次埋点先写入localStorage,每 30s 合并一次并用普通fetch上报,卸载时再用sendBeacon补最后一条 - 降级用
image打点:new Image().src = "https://log.example.com/pixel.gif?e=close&ts=" + Date.now(),兼容性最好,但受 Referer 策略和 CORS 影响 - 避免用
navigator.sendBeacon发送大体积日志(如完整 error stack),拆成多次小请求,或压缩后再发
最关键的一点:不要假设「调用了就等于服务端收到了」。Beacon 的设计哲学是「尽力而为」,它的可靠性取决于浏览器实现、网络状态、系统资源,以及你有没有在正确的时间、用正确的格式、塞进正确的数据量。线上监控必须覆盖服务端接收率、客户端调用成功率(sendBeacon() 返回值)、以及前后端 ID 对账。










