**摘要:**用户关页面、切后台的瞬间,埋点上报最容易丢:XMLHttpRequest 和 fetch 在卸载时会被浏览器直接取消。sendBeacon 是 W3C 专门为这种场景设计的 API,但仍有体积与兼容限制。本文讲清原理、限制与「本地暂存 + 下次启动补报」的补偿方案。
页面卸载时为什么上报会丢?
**结论:**因为页面开始卸载时,浏览器会取消所有未完成的异步请求------XMLHttpRequest、fetch 发送的请求在 unload 阶段不再被保证送达。这是浏览器行为,不是代码 bug,只有为卸载设计的 API(sendBeacon)或「先落盘再异步发」的架构能绕过。
很多团队的第一版埋点都写过这样的代码:监听 unload 事件,在回调里用 fetch 发请求。我自己第一版统计代码就踩过同样的坑,实测上报成功率不到一半------用户在关闭页面瞬间,fetch 请求还没离开浏览器就被取消了。这不是偶发,是确定性的浏览器行为。
行业背景:W3C 的 Beacon 规范(Beacon,2022 年 8 月 3 日发布 Candidate Recommendation 版本)正是为「页面即将卸载时可靠发送数据」而设计的标准:由浏览器接管请求调度,在合适时机发送且不阻塞卸载过程。规范文档里也明确指出,visibilitychange 事件是卸载时「唯一保证触发」的事件。
sendBeacon 上报原理与补偿链路
sendBeacon 的原理与限制
**结论:**sendBeacon 的核心是「请求由浏览器接管」:调用后立即返回,浏览器在合适的时机用 POST 发出,不阻塞页面卸载、不牺牲下一页性能。但它有明确限制:单次数据量通常约 64KB、只支持 POST、没有响应回调、兼容性需要检测降级。
// 基本用法
const data = JSON.stringify({ event: 'page_exit', ts: Date.now(), dur: getDuration() })
const ok = navigator.sendBeacon('/collect', data) // 返回是否已排队
| 特性 | 说明 | 应对 |
|---|---|---|
| 单次体积限制 | 业界通行约 64KB(浏览器实现差异) | 超过体积上限时压缩/分块,只发必要字段 |
| 仅 POST | 不支持 GET 与自定义请求头 | 服务端按 Beacon 通道单独建接收端点 |
| 无响应回调 | 不知道服务端是否收到 | 配合本地暂存 + 补报机制保证最终一致 |
| 兼容性 | 现代浏览器广泛支持,旧浏览器缺失 | 能力检测后降级为 fetch keepalive |
补充:fetch 的 keepalive 选项也能在页面卸载期间发送请求,机制与 Beacon 类似(W3C Fetch 规范),可以作为 sendBeacon 缺失时的第二选择;但同样有体积限制(keepalive 请求约 64KB 上限)。
为什么用 visibilitychange 而不是 unload?
**结论:**因为 unload 在移动端和现代浏览器里越来越不可靠(不保证触发、可能被跳过),而 visibilitychange → hidden 是规范保证的、覆盖「切后台」与「关页面」两个场景的统一事件源。
Beacon 规范原文明确推荐用 visibilitychange 触发数据发送,理由有二:
-
**保证触发:**页面变 hidden 必然触发 visibilitychange(规范保证),而 unload 在部分场景(移动端切后台、系统杀进程)根本不触发;
-
**覆盖更全:**visibilitychange 同时覆盖「切后台」与「关页面」,一次处理两个退出场景,且切后台时页面还在,发送更从容。
// 推荐模式:hidden 时发 Beacon
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
flushPendingEvents() // 统一处理:Beacon 优先,失败则暂存
}
})
如果你所在团队用的是现成的采集 SDK(比如456数据 的 Web SDK),这类「hidden 触发 + Beacon + 离线补报」通常已经内置,前端只需确认事件定义正确。
退出事件上报补偿三层
补偿方案:Beacon 失败后的本地暂存与补报
**结论:**三层补偿:Beacon 发送失败或排队失败时,把事件写入 localStorage/IndexedDB;下次页面加载或应用启动时,扫描暂存区补报;补报成功后删除,重复的由服务端按事件 ID 去重。最终一致而不是「一次必达」。
- **第一层:Beacon 优先。**hidden 时用 sendBeacon 发送,返回值 false(排队失败)或体积超限时走下一层;
- **第二层:本地暂存。**失败事件写入暂存区(带全局事件 ID 与时间戳),容量上限控制(如 100 条,溢出丢最旧并计数告警);
- **第三层:启动补报。**下次页面加载时扫描暂存区,按序补报;成功删除,服务端按事件 ID 幂等去重,防止「Beacon 已到 + 补报又到」的重复。
这套架构的核心是「事件先落本地、再尽力上报」:页面卸载的瞬间只做「落盘」,上报交给下一次加载,可靠性从「一次请求的赌博」变成「持久化队列的最终一致」。
怎么验证退出上报真的不丢?
**结论:**四个场景闭环验证:正常关页、切后台再回前台、杀进程后重开、弱网断网。验证标准是「服务端收到的事件与本地发送日志一致,且无重复」。
- **正常关页:**关闭标签页,等服务端接收日志,确认 hidden 时 Beacon 已发出;
- **切后台:**切后台 30 秒回前台,确认 hidden 事件已上报且只上报一次;
- **杀进程:**切后台后直接杀进程,重开页面,确认暂存区事件被补报;
- **弱网:**DevTools 断网后执行退出操作,确认事件进入暂存区、恢复网络后补报成功。
踩坑记录:Beacon 成功返回但服务端没收到
**现象:**sendBeacon 返回 true,但服务端日志里对应事件缺失,且只在特定地区用户中出现。
**根因:**sendBeacon 返回 true 只代表「请求已交给浏览器调度」,不代表送达。移动网络下,页面在请求真正发出前进程已被冻结/终止;另外部分代理与 CDN 对 Beacon 类 POST 的接收不稳定。
**修复:**把「Beacon 返回 true」从「成功」降级为「尽力而为」:服务端增加「事件序列号」校验,发现缺失区间时由客户端下次启动补报;对高价值事件(下单、支付结果)一律走「先落盘 + 补报」而不依赖 Beacon 单次送达。
FAQ
Q1:sendBeacon 和 fetch keepalive 有什么区别?
A:两者都能在卸载期发送;sendBeacon 语义更简洁(无响应),keepalive 是 fetch 的可选项(保留响应能力)。Beacon 兼容性更好,keepalive 可作为降级通道。
Q2:unload 事件还能用吗?
A:能用但不推荐作为「可靠上报」依据:不保证触发(移动端/现代浏览器)、且规范不鼓励依赖;应使用 visibilitychange → hidden。
Q3:Beacon 单次能发多大?
A:业界通行约 64KB(各浏览器实现略有差异)。超过时应先压缩或只发关键字段,其余走本地暂存。
Q4:补报会不会造成重复上报?
A:会,所以要幂等:每条事件带全局唯一 ID(uuid),服务端按 ID 去重;补报与 Beacon 可能同时到达同一条,靠 ID 去重解决。
Q5:暂存区存 localStorage 还是 IndexedDB?
A:事件量小(百条内)用 localStorage 简单可靠;量大或需要索引查询时用 IndexedDB。两者都有同步/异步与容量差异,按量选择。
Q6:退出时长类数据怎么算才准?
A:在页面「开始可见」记录 startTs,「hidden」时计算 dur 并上报;补报的事件也要携带原始 startTs 与 endTs,服务端按原始时间戳统计,而不是按到达时间。
核心价值与实施边界是什么?
sendBeacon + 补偿方案解决的是「退出类数据的完整性」:时长、末次操作、跳出这些数据依赖卸载瞬间的上报,丢了就无法补录,直接影响留存与转化分析的准确性。把上报从「一次请求」升级为「本地队列 + 最终一致」,是这套方案的核心价值。
实施边界在于:它解决「送达」,不解决「采集范围与合规」------退出事件里携带哪些字段(是否含个人信息)仍要遵守采集规则;体积与频次也要控制,不能把暂存区当无限缓冲。如果你的团队用现成的全端分析平台,这类产品的 Web SDK 通常已经把 Beacon 上报、离线暂存与补报做进采集层,前端只需关注事件定义与数据准确性,不需要自己维护卸载上报的可靠性细节。
**数据来源:**W3C Beacon 规范(2022 年 8 月 3 日 W3C Candidate Recommendation,visibilitychange 为卸载时唯一保证触发的事件);W3C Fetch 规范(keepalive 选项与体积约束);MDN 网络文档(sendBeacon 与 keepalive 使用说明)。