基于Beacon API的前端日志极端情况下“最后一眼”收集方案
前端“最后一眼”日志:为什么非用 Beacon 不可? 页面即将关闭、刷新或跳转时,常规的 fetch 或 XMLHttpRequest 几乎注定被浏览器直接中断,日志也随之丢失。此时,navigator.sendBeacon 几乎是唯一靠谱的选择——规范白纸黑字写明:即使页面已不可见,它也会尽力将
前端“最后一眼”日志:为什么非用 Beacon 不可?
页面即将关闭、刷新或跳转时,常规的 fetch 或 XMLHttpRequest 几乎注定被浏览器直接中断,日志也随之丢失。此时,navigator.sendBeacon 几乎是唯一靠谱的选择——规范白纸黑字写明:即使页面已不可见,它也会尽力将请求发送出去。并且,它不阻塞卸载流程、不依赖事件循环,在 Chrome 39+、Firefox 31+、Safari 11.1+、Edge 79+ 等主流浏览器中稳定可用。
当然,存在两个硬性限制:仅支持 POST 方法,payload 必须是 ArrayBuffer、Blob、FormData 或 URLSearchParams 类型;直接传递普通字符串或 JSON 对象会静默失败,连错误提示都不会给出。

合法且兼容的 Beacon 日志 payload 构造方法
千万不要直接 JSON.stringify(logData) 作为参数传入,那会触发类型错误。必须显式转换为允许的类型:
- 优先使用
Blob:new Blob([JSON.stringify(logData)], { type: 'application/json' })——兼容性好,服务端收到的 Content-Type 明确,解析无歧义。 - 若后端只接受表单格式:可使用
new URLSearchParams({ data: JSON.stringify(logData) }),但需注意 URL 编码长度限制(一般 ≤2048 字符)。 - 避免使用
FormData:在部分 Safari 版本中,卸载阶段 FormData 可能失效,且服务端需额外处理 multipart 解析,增加复杂度。 - payload 体积控制在 64KB 以内:过大容易被浏览器截断,发送失败概率明显上升。
Beacon 上报前必须完成的三个步骤
Beacon 并非直接发送后就无需关注。遗漏任何一步,日志都可能无法发出或内容残缺:
- 必须在
beforeunload或pagehide事件中直接调用,不能放在异步回调中(例如setTimeout(() => sendBeacon(...), 0))——事件上下文一旦退出,Beacon 即失效。 - 必须检查
navigator.sendBeacon是否存在:旧版 IE 及部分 WebView 不支持,需要 fallback 到new Image().src = '/logdata=...'方案。但需注意 URL 长度与编码安全。 - 日志数据需提前准备好:不要在
beforeunload中临时计算性能指标(如getPerformanceMetrics())或读取localStorage——这些 I/O 操作很可能来不及完成。
边界问题:页面快速跳转与多 Beacon 竞争
用户连续点击两个链接时,A 页面的 beforeunload 触发,B 页面的 pagehide 触发,但浏览器对同一页面只允许一次 Beacon 发送。如果 A 尚未发送完毕就跳转到 B,B 的 Beacon 会覆盖 A 的请求,导致 A 的“最后一眼”日志丢失。
解决方案并非加锁,而是收敛行为:
- 仅在
pagehide中发送 Beacon——它比beforeunload更可靠,还能捕获缓存导航。 - 结合
document.hidden与visibilitychange:判断是否真正离开当前域(排除标签页切换等伪卸载场景)。 - 关键日志(如 JS 错误、支付失败)应在发生时立即使用 Beacon 发送,不要等到卸载——卸载仅作为兜底方案,而非主通道。
真正棘手的是单页应用中的路由跳转:它不触发卸载,因此“最后一眼”日志并不存在。在此场景下,只能依靠主动打点加定期 flush,Beacon 仅负责收集真正的终局日志。
