每个前端开发者都踩过这个坑:用户点击关闭标签页,你精心采集的分析数据、最后一条聊天消息、或者表单草稿,就这么无声无息地丢了。navigator.sendBeacon() 看似解决了问题,实际上它有致命缺陷——无法设置请求头、无法读取响应、无法取消。Chrome 133 起正式推出的 fetchLater() API(Pending Beacon)彻底改变了这个局面。本文将从问题根源出发,用完整代码演示如何用 fetchLater() 实现可靠的页面卸载数据发送,并深入分析它与 sendBeacon()、keepalive fetch 的性能与可靠性对比。
🔍 一、为什么 sendBeacon 不够用
sendBeacon 的三大硬伤
navigator.sendBeacon() 自 2014 年推出以来,一直是处理页面卸载时数据发送的事实标准。但在实际生产中,它暴露了三个无法回避的问题:
- 无法设置请求头——你不能指定
Content-Type: application/json,只能发送text/plain、application/x-www-form-urlencoded等有限类型 - 无法获取响应——
sendBeacon返回boolean,你永远不知道服务端是否真正接收并处理了数据 - 无法取消或重试——一旦调用,请求就飞出去了,没有 AbortController,没有重试机制
// ❌ sendBeacon 的局限:无法设置自定义请求头,无法读取响应
const success = navigator.sendBeacon('/api/analytics', JSON.stringify({
event: 'page_close',
timestamp: Date.now(),
duration: performance.now()
}));
// success 只表示浏览器"接受了这个请求",不代表服务端成功接收
console.log(success); // true — 但这毫无意义
⚠️ 警告:
sendBeacon的success返回值仅表示浏览器接受了该请求的入队操作,不代表网络传输成功,更不代表服务端处理成功。不要用它来做可靠性判断。
keepalive fetch 的代价
另一个常见方案是 fetch() 配合 keepalive: true,它解决了请求头的问题,但引入了新的限制:
// ❌ keepalive fetch 有 64KB 的请求体大小限制
fetch('/api/analytics', {
method: 'POST',
keepalive: true,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(largePayload) // 超过 64KB 会被静默丢弃!
});
| 特性 | sendBeacon | keepalive fetch | fetchLater() |
|---|---|---|---|
| 自定义请求头 | ❌ 不支持 | ✅ 支持 | ✅ 支持 |
| 读取响应 | ❌ 不支持 | ❌ 页面关闭后不可用 | ✅ 下次访问可读 |
| 取消请求 | ❌ 不支持 | ✅ AbortController | ✅ 可取消 |
| 请求体大小限制 | ~64KB | 64KB | 浏览器决定,通常更大 |
| 页面关闭后发送 | ✅ 浏览器托管 | ✅ 浏览器托管 | ✅ 浏览器托管 |
| 可延迟发送 | ❌ 立即发送 | ❌ 立即发送 | ✅ 可指定时间 |
| 重试机制 | ❌ 无 | ❌ 无 | ✅ 浏览器自动重试 |
| 浏览器支持 | 所有现代浏览器 | 所有现代浏览器 | Chrome 133+ |
⚡ 关键结论:fetchLater() 是目前唯一能在页面关闭后仍然可靠发送请求、且支持自定义头和响应读取的浏览器原生 API。
🚀 二、fetchLater API 核心用法
基础语法与生命周期
fetchLater() 的核心设计理念是:你告诉浏览器"这个请求很重要,请在合适的时候帮我发出去"。浏览器会智能地选择发送时机——可以立即发送,也可以延迟到页面卸载时发送。
// ✅ fetchLater 基础用法:页面关闭时自动发送
const beacon = fetchLater('/api/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'session_end',
sessionId: crypto.randomUUID(),
duration: Math.round(performance.now()),
page: location.pathname
}),
activateAfter: 0 // 立即激活(默认值),浏览器可随时发送
});
// 查询请求状态
console.log(beacon.state); // 'pending' | 'sent' | 'aborted'
fetchLater() 返回一个 PendingBeacon 对象,它有三个状态:
pending——请求已入队,等待发送sent——请求已成功发送aborted——请求已被取消
activateAfter:精确控制发送时机
activateAfter 参数是 fetchLater() 最独特的能力——你可以告诉浏览器"在 N 毫秒后再考虑发送这个请求"。这在分析场景中极其有用:
// ✅ 场景:用户可能误触关闭,延迟 5 秒后再发送会话数据
const sessionBeacon = fetchLater('/api/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(getSessionData()),
activateAfter: 5000 // 5 秒后才激活,浏览器可在之后的任意时刻发送
});
// 如果用户在 5 秒内重新打开页面(比如误触恢复),可以取消
window.addEventListener('pageshow', (e) => {
if (e.persisted && sessionBeacon.state === 'pending') {
sessionBeacon.deactivate(); // 取消发送,会话还在继续
console.log('用户回来了,取消会话结束上报');
}
});
💡 提示:
activateAfter不是"在 N 毫秒后发送",而是"在 N 毫秒后允许发送"。浏览器可能在激活后才真正发送,具体时机由浏览器根据网络状况、电量等因素决定。
取消与重试机制
与 sendBeacon 最大的区别之一是 fetchLater() 支持取消:
// ✅ 有条件地取消 PendingBeacon
const checkoutBeacon = fetchLater('/api/checkout-abandoned', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cart: getCartItems(),
total: calculateTotal(),
abandonTime: Date.now()
}),
activateAfter: 30000 // 30 秒后才发送"购物车放弃"事件
});
// 用户完成了结账,取消"放弃"事件
document.getElementById('checkout-btn').addEventListener('click', async () => {
if (checkoutBeacon.state === 'pending') {
checkoutBeacon.deactivate(); // 取消
}
await processCheckout();
});
💡 三、生产环境实战场景
场景一:可靠的分析数据上报
分析数据上报是最经典的 use case。传统的做法是在 beforeunload 中调用 sendBeacon,但这会导致请求丢失率在 5-15% 之间。fetchLater() 将丢失率降低到接近 0:
// ✅ 分析 SDK 核心上报逻辑
class AnalyticsSDK {
#beacons = [];
#sessionId = crypto.randomUUID();
#startTime = performance.now();
track(event, properties = {}) {
const payload = {
event,
properties,
sessionId: this.#sessionId,
timestamp: Date.now(),
url: location.href,
userAgent: navigator.userAgent
};
// 常规分析事件用 sendBeacon 即可(快速、轻量)
if (event !== 'session_end') {
navigator.sendBeacon('/api/track', JSON.stringify(payload));
return;
}
// 会话结束事件用 fetchLater,确保可靠送达
const beacon = fetchLater('/api/track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...payload,
properties: {
...properties,
sessionDuration: Math.round(performance.now() - this.#startTime)
}
}),
activateAfter: 2000 // 2 秒后激活,避免误关闭
});
this.#beacons.push(beacon);
return beacon;
}
// 获取所有待发送的 beacon 状态
getStatus() {
return this.#beacons.map(b => ({
state: b.state,
url: b.url
}));
}
}
// 使用
const analytics = new AnalyticsSDK();
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
analytics.track('session_end', {
scrollDepth: getScrollDepth(),
interacted: hasUserInteracted()
});
}
});
场景二:表单草稿自动保存
用户填写了一半的长表单,突然关闭了页面——这种场景用 fetchLater() 可以实现无感知的草稿保存:
// ✅ 表单草稿自动保存
class DraftSaver {
#form;
#beacon = null;
#debounceTimer = null;
constructor(formSelector) {
this.#form = document.querySelector(formSelector);
this.#setupListeners();
}
#setupListeners() {
// 用户输入时防抖保存到 localStorage
this.#form.addEventListener('input', () => {
clearTimeout(this.#debounceTimer);
this.#debounceTimer = setTimeout(() => {
localStorage.setItem('form_draft', this.#getFormData());
}, 1000);
});
// 页面关闭时用 fetchLater 发送到服务端
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.#saveDraftToServer();
}
});
}
#getFormData() {
return JSON.stringify(Object.fromEntries(new FormData(this.#form)));
}
#saveDraftToServer() {
const draft = this.#getFormData();
if (!draft || draft === '{}') return;
// 取消之前的 beacon(如果有)
if (this.#beacon?.state === 'pending') {
this.#beacon.deactivate();
}
this.#beacon = fetchLater('/api/drafts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Draft-Id': crypto.randomUUID()
},
body: draft,
activateAfter: 1000
});
}
// 用户回来时恢复草稿
async restoreDraft() {
try {
const res = await fetch('/api/drafts/latest');
if (res.ok) {
const draft = await res.json();
Object.entries(draft).forEach(([name, value]) => {
const el = this.#form.querySelector(`[name="${name}"]`);
if (el) el.value = value;
});
}
} catch {
// fallback to localStorage
const local = localStorage.getItem('form_draft');
if (local) {
Object.entries(JSON.parse(local)).forEach(([name, value]) => {
const el = this.#form.querySelector(`[name="${name}"]`);
if (el) el.value = value;
});
}
}
}
}
场景三:实时协作文档的最终快照
在使用 CRDT 的实时协作编辑器中,用户关闭页面时需要发送最终的文档快照。这个请求必须可靠,否则会丢失最后一次编辑:
// ✅ 协作文档最终快照
class CollabDocument {
#docId;
#beacon = null;
constructor(docId) {
this.#docId = docId;
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.#sendFinalSnapshot();
}
});
}
#sendFinalSnapshot() {
const snapshot = this.#getDocumentState();
if (!snapshot.dirty) return; // 没有未保存的修改
this.#beacon = fetchLater(`/api/docs/${this.#docId}/snapshot`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Snapshot-Version': String(snapshot.version)
},
body: JSON.stringify({
content: snapshot.content,
cursors: snapshot.cursors,
timestamp: Date.now()
}),
activateAfter: 3000 // 3 秒后激活,给网络波动留缓冲
});
// 同时写入 IndexedDB 作为双保险
this.#saveToLocal(snapshot);
}
#getDocumentState() {
// 返回当前文档状态
return { content: '', cursors: [], version: 0, dirty: false };
}
#saveToLocal(snapshot) {
const tx = indexedDB.transaction('docs', 'readwrite');
tx.objectStore('docs').put({
id: this.#docId,
...snapshot,
savedAt: Date.now()
});
}
}
📌 **记住:**对于关键数据(如支付、文档保存),永远不要只依赖
fetchLater()。应该同时写入 IndexedDB 作为本地备份,下次页面加载时检查并恢复。
⚠️ 四、浏览器兼容性与降级方案
fetchLater() 目前(2026 年 6 月)在 Chrome 133+ 和 Edge 133+ 中可用,Firefox 和 Safari 尚未实现。在生产环境中必须提供降级方案:
// ✅ 通用可靠发送工具函数:自动降级
function reliableSend(url, data, options = {}) {
const { activateAfter = 0, headers = {} } = options;
const body = typeof data === 'string' ? data : JSON.stringify(data);
// 优先使用 fetchLater
if ('fetchLater' in globalThis) {
return {
type: 'fetchLater',
beacon: fetchLater(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers
},
body,
activateAfter
})
};
}
// 降级:keepalive fetch
if (typeof globalThis.fetch === 'function') {
return {
type: 'keepalive',
promise: fetch(url, {
method: 'POST',
keepalive: true,
headers: {
'Content-Type': 'application/json',
...headers
},
body
})
};
}
// 最终降级:sendBeacon
return {
type: 'beacon',
success: navigator.sendBeacon(url, new Blob([body], {
type: 'application/json'
}))
};
}
// 使用示例
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
const result = reliableSend('/api/track', {
event: 'page_hide',
timestamp: Date.now()
}, { activateAfter: 2000 });
console.log(`使用 ${result.type} 方式发送`);
}
});
| 方案 | 请求头自定义 | 响应读取 | 可取消 | 可延迟 | 兼容性 |
|---|---|---|---|---|---|
| fetchLater() | ✅ | ✅ | ✅ | ✅ | Chrome 133+ |
| keepalive fetch | ✅ | ❌ | ✅ | ❌ | 所有现代浏览器 |
| sendBeacon | ❌ | ❌ | ❌ | ❌ | 所有现代浏览器 |
| beforeunload fetch | ✅ | ❌ | ❌ | ❌ | 所有,但不可靠 |
⚠️ **警告:**永远不要在
beforeunload事件处理函数中发起同步XMLHttpRequest。虽然它曾经是可靠发送数据的方式,但现代浏览器已经限制或完全阻止这种行为,因为它会阻塞页面关闭,严重影响用户体验。
💡 五、最佳实践与避坑指南
✅ 推荐做法
- 会话结束事件用
fetchLater,普通事件用sendBeacon——fetchLater有额外的内存开销,不需要对每个分析事件都使用 - 设置合理的
activateAfter——2-5 秒可以过滤误关闭,但不要设置太长,否则用户已经离开很久了数据还没发出去 - 配合 IndexedDB 做双保险——
fetchLater只有在网络可用时才能发送,离线场景下需要本地存储兜底 - 在
pageshow事件中检查并取消——如果用户通过浏览器"恢复标签页"功能回来,应该取消待发送的 beacon
❌ 避免做法
- 不要为每个用户交互都创建
fetchLater——每个 beacon 都会占用内存,大量 beacon 可能导致内存压力 - 不要依赖
fetchLater传输大数据——虽然没有像keepalive那样明确的 64KB 限制,但浏览器仍可能拒绝过大的请求体 - 不要假设
fetchLater一定成功——网络断开、浏览器崩溃等极端情况下数据仍可能丢失 - 不要在 Service Worker 中使用——
fetchLater()是 window 上下文的 API,Service Worker 中不可用
性能注意事项
// ❌ 错误:为每个事件都创建 beacon
events.forEach(event => {
fetchLater('/api/track', {
method: 'POST',
body: JSON.stringify(event)
});
});
// ✅ 正确:批量合并后创建单个 beacon
const batchedEvents = events.map(e => JSON.stringify(e)).join('\n');
fetchLater('/api/track/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/x-ndjson' },
body: batchedEvents
});
📊 总结
fetchLater() API 是浏览器原生的数据可靠性方案的一次重要升级。它不是要完全替代 sendBeacon(),而是在关键场景(会话结束、表单草稿、最终快照)中提供更可靠的保障。
对于 jsjson.com 这类纯前端工具站来说,fetchLater() 最直接的应用场景是用户行为分析——确保用户关闭页面前的最后一次操作(比如使用了哪个工具、生成了什么数据)能被可靠记录,从而优化工具的使用体验。
推荐的发送策略分层:
| 数据类型 | 推荐方案 | 原因 |
|---|---|---|
| 普通事件(点击、浏览) | sendBeacon | 轻量、快速、足够可靠 |
| 关键事件(会话结束、放弃购物车) | fetchLater | 可靠性最高,支持延迟 |
| 需要重试的数据 | keepalive fetch + 本地存储 | 可编程重试逻辑 |
| 大批量历史数据 | IndexedDB + 后台同步 | 离线可用 |
⚡ **关键结论:**在 2026 年的前端架构中,
fetchLater()应该成为你处理页面卸载数据发送的首选方案。配合sendBeacon做日常事件上报、IndexedDB 做离线兜底,你就拥有了一个从"尽力而为"到"可靠送达"的完整数据上报体系。