navigator.sendBeacon更适合“最后上报”,因为它在页面卸载时以无阻塞、不可取消、不依赖响应的方式异步发送POST请求,被浏览器列为高优先级后台任务,不触发CORS预检,且不阻塞卸载流程。

使用 navigator.sendBeacon 实现用户离开页面前的“最后存盘”,核心在于它能在页面卸载(unload / beforeunload)阶段异步发出一个**无阻塞、不可取消、不依赖响应**的 POST 请求,且浏览器保证其发送完成(或尽力而为)。这比 fetch 或 XMLHttpRequest 在卸载时更可靠。
为什么 sendBeacon 更适合“最后上报”
sendBeacon 的设计目标就是解决卸载场景下的数据上报问题:
- 它被浏览器列入“高优先级后台任务”,即使 tab 被关闭或导航跳转,仍会尝试在进程终止前发出请求
- 不等待服务器响应,不触发 CORS 预检(只支持简单请求:
text/plain、application/x-www-form-urlencoded、multipart/form-data),也不受页面生命周期中断影响 - 不会阻塞页面卸载,用户感知不到延迟;而
fetch(...).then(...)在beforeunload中大概率被中止 - 返回
true表示已成功入队(非发送成功),false表示因资源限制(如内存不足)或不支持而失败(可降级)
正确注册卸载时机并构造数据
必须在 pagehide(推荐)或 visibilitychange(隐藏时)或 beforeunload(兼容旧版)中调用,避免在 unload 中调用(部分浏览器已禁用):
-
首选
pagehide:涵盖页面隐藏、刷新、关闭、导航等所有卸载路径,且事件可被监听到 - 数据建议序列化为
URLSearchParams(自动适配application/x-www-form-urlencoded)或FormData(支持文件字段,但注意体积) - 避免传大对象或未处理的二进制数据;若需 JSON,可转成字符串后作为字段值,例如:
params.append('data', JSON.stringify(obj))
关键健壮性保障措施
sendBeacon 不是银弹,需配合以下策略提升成功率:
-
服务端幂等设计:同一用户+同一会话的“最后存盘”可能因重试或多次触发(如刷新+关闭)而重复到达,后端应基于
session_id+timestamp或带 hash 的 payload 去重或取最新 -
前端防重复触发:用闭包变量或
WeakMap标记是否已发送,避免pagehide+beforeunload同时触发两次 -
降级 fallback:检测
!navigator.sendBeacon或调用返回false时,可尝试同步XMLHttpRequest(仅限beforeunload,有阻塞风险)或记录到localStorage,下次打开时补报 -
精简 payload:控制在 64KB 内(Chrome 限制),避免因超限静默失败;关键字段如
user_id、doc_id、last_edit_ms、content_hash即可
一个轻量可用的封装示例
以下代码兼顾兼容性、防重与降级:
let beaconSent = false;
const sendLastSave = (url, data) => {
if (beaconSent) return;
beaconSent = true;
const params = new URLSearchParams();
Object.entries(data).forEach(([k, v]) => params.append(k, v));
// 尝试 sendBeacon
if (navigator.sendBeacon && navigator.sendBeacon(url, params)) {
return;
}
// 降级:beforeunload 中同步发(仅当无 pagehide 时才可能走到这里)
if (window.performance && performance.navigation && performance.navigation.type === 1) {
// 刷新场景,可考虑 localStorage 暂存 + 下次恢复
localStorage.setItem('pending_last_save', params.toString());
}
};
// 监听 pagehide(主通道)
window.addEventListener('pagehide', () => {
sendLastSave('/api/last-save', {
user_id: 'u123',
doc_id: 'd456',
timestamp: Date.now(),
content_hash: 'abc789'
});
});

















