在用户退出浏览器前可靠地发送积压的埋点数据是前端监控系统的关键需求。以下是几种实现方案及其优缺点分析,结合实际场景给出最优方案:
一、传统方案:beforeunload/unload事件
原理:监听页面卸载事件,在回调中发送数据。
问题:
- 同步请求会阻塞页面关闭,体验差
- 异步请求(如fetch)可能被浏览器中断
- 移动端兼容性差(iOS Safari会忽略大部分回调)
window.addEventListener('beforeunload', () => {
// 同步XHR(阻塞页面,不推荐)
const xhr = new XMLHttpRequest();
xhr.open('POST', '/analytics', false);
xhr.send(JSON.stringify(pendingData));
});
二、最优方案:navigator.sendBeacon
原理:HTML5 API,专门设计用于在页面卸载时异步发送小数据。
优点:
- 非阻塞,不影响用户体验
- 浏览器保证尝试发送(放入特殊队列)
- 兼容性良好(Chrome 39+、Firefox 31+、Safari 11+)
window.addEventListener('unload', () => {
if (pendingData.length > 0) {
navigator.sendBeacon('/analytics', JSON.stringify(pendingData));
}
});
优化建议:
- 批量发送:将多次埋点数据缓存,退出前一次性发送
- 数据压缩:使用
btoa(unescape(encodeURIComponent(data)))
进行Base64编码 - 失败重试:结合IndexedDB存储未成功发送的数据
三、进阶方案:fetch + keepalive
原理:现代浏览器支持的fetch
扩展,通过keepalive
标志告诉浏览器即使页面关闭也要继续发送请求。
优点:
- 支持更大数据量(相比sendBeacon的64KB限制)
- 可获取服务器响应(但页面卸载前可能无法处理)
window.addEventListener('unload', async () => {
if (pendingData.length > 0) {
await fetch('/analytics', {
method: 'POST',
body: JSON.stringify(pendingData),
keepalive: true // 关键参数
});
}
});
兼容性:Chrome 59+、Firefox 69+、Safari 14.1+
四、数据持久化:IndexedDB + 定时同步
原理:
- 将埋点数据存入IndexedDB(持久化存储)
- 页面活跃时定时发送(如每5分钟)
- 页面卸载时触发最后一次发送
// 1. 埋点数据存入IndexedDB
async function saveEvent(event) {
const db = await openDatabase();
const tx = db.transaction('events', 'readwrite');
tx.objectStore('events').add(event);
return tx.done;
}
// 2. 定时同步(如使用Service Worker)
self.addEventListener('periodicsync', event => {
event.waitUntil(sendPendingEvents());
});
// 3. 页面卸载时触发
window.addEventListener('unload', () => {
sendPendingEvents().then(() => {
navigator.sendBeacon('/analytics', 'flush');
});
});
五、混合方案:多重保障机制
推荐实现:结合多种方法提高可靠性
class AnalyticsTracker {
constructor() {
this.pendingData = [];
this.db = null;
// 初始化IndexedDB
this.initDatabase();
// 监听页面卸载
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.flushData();
}
});
// 定时同步
setInterval(() => this.flushData(), 30000);
}
async initDatabase() {
this.db = await idb.openDB('analyticsDB', 1, {
upgrade(db) {
db.createObjectStore('events', { keyPath: 'id', autoIncrement: true });
}
});
}
trackEvent(event) {
this.pendingData.push(event);
this.saveToDB(event);
}
async saveToDB(event) {
if (this.db) {
await this.db.add('events', event);
}
}
async flushData() {
const allData = await this.getAllEvents();
if (allData.length === 0) return;
try {
// 优先使用fetch+keepalive
const response = await fetch('/analytics', {
method: 'POST',
body: JSON.stringify(allData),
keepalive: true
});
if (response.ok) {
await this.clearEvents();
}
} catch (error) {
// 降级使用sendBeacon
navigator.sendBeacon('/analytics', JSON.stringify(allData));
}
}
async getAllEvents() {
if (!this.db) return [];
return await this.db.getAll('events');
}
async clearEvents() {
if (this.db) {
await this.db.clear('events');
}
}
}
六、关键注意事项
-
数据量控制:
- sendBeacon建议<64KB
- 超过1MB数据建议分批次发送
-
兼容性处理:
function sendFinalData(data) { if (navigator.sendBeacon) { return navigator.sendBeacon('/analytics', data); } else if ('fetch' in window) { return fetch('/analytics', { method: 'POST', body: data, keepalive: true }); } else { // 传统同步XHR(最后手段) const xhr = new XMLHttpRequest(); xhr.open('POST', '/analytics', false); xhr.send(data); return xhr.status === 200; } }
-
移动端特殊处理:
- iOS Safari限制:使用
pagehide
事件代替unload
- 微信内置浏览器:监听
onpageshow
和onpagehide
- iOS Safari限制:使用
-
调试技巧:
- Chrome DevTools → Network → Preserve log
- 使用Service Worker拦截并记录请求
七、总结
- 优先方案:
navigator.sendBeacon
+ IndexedDB持久化 - 现代浏览器:
fetch + keepalive
- 兼容性降级:同步XHR(仅作为最后的保底手段)
通过多重保障机制,可以将埋点数据发送成功率从约70%(仅使用unload)提升到95%以上,有效解决用户退出时数据丢失的问题。