做前端监控和埋点的人几乎都遇到过同一个问题:用户点了关闭按钮,或者跳转到另一个站点,我们在unload事件里发出的统计请求被浏览器无情地砍掉了。数据丢了,报表就少了,排查半天才发现是发送时机的问题。Beacon API就是为这个场景而生的,它的核心方法navigator.sendBeacon可以在页面卸载时把数据可靠地“托付”给浏览器,由浏览器保证把请求送出去,而页面本身不需要等待任何响应。

sendBeacon的底层机制:为什么它能保证送达
先说清楚一个容易被误解的点:sendBeacon并不是什么魔法通道,它发出的仍然是一个普通的HTTP POST请求。区别在于请求的生命周期归属。当你调用XMLHttpRequest或fetch时,请求的生命周期绑定在页面上,页面一销毁,尚未完成的请求就会被终止。而sendBeacon发出的请求生命周期归属于浏览器进程本身,你只是把数据交给浏览器,告诉它“这件事帮我办完”,之后页面就算立刻消失,浏览器也会在后台继续完成这次传输。
正因为不需要等待响应,sendBeacon的设计是完全异步且fire-and-forget的。它的返回值是一个布尔值,表示浏览器是否接受了这次投递任务,而不是请求是否成功。这个细节很关键:你永远无法通过它知道服务端处理结果,也没有任何回调或Promise可用。所以它只适合上报那种“丢了有点可惜但不致命”的数据,比如PV、UV、点击行为、停留时长这类统计信息。
还有一点,sendBeacon会继承当前页面的Cookie和认证上下文,请求在页面卸载时排队发出,并且不占用页面主线程的渲染时间。它的优先级通常较低,不会和关键资源加载抢占带宽,这也是它比在unload里强行发AJAX体验更好的原因之一。
与XMLHttpRequest、fetch keepalive的对比
传统方案是在unload或beforeunload里用同步AJAX硬扛,这种写法不仅会阻塞页面关闭造成卡顿,而且在现代浏览器里几乎必然失败,因为页面卸载时未完成的异步请求会被取消,而同步请求又被各大浏览器明确废弃了。
// 不可靠的传统写法:页面卸载时大概率被中断
window.addEventListener('unload', function () {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/collect', false); // 同步请求已被废弃
xhr.send(JSON.stringify({ page: '/home', t: Date.now() }));
});fetch提供了一个keepalive选项,功能上和Beacon类似,也能让请求在页面卸载后继续完成。它的优势是支持完整的请求定制,比如可以自定义header、使用GET之外的复杂场景、还能拿到响应Promise。缺点是兼容性略逊于sendBeacon,而且API调用相对啰嗦。两者的能力边界如下表所示:
| 特性 | sendBeacon | fetch keepalive | 普通XHR |
|---|---|---|---|
| 页面卸载后可送达 | 是 | 是 | 否 |
| 可读响应内容 | 否 | 是 | |
| 可设置自定义Header | 否 | 是 | 是 |
| 请求方法 | 仅POST | 任意 | 任意 |
一个简单的经验法则:如果只是单纯扔一段统计数据出去、不关心结果,优先用sendBeacon;如果需要自定义头部(比如带token的Authorization)或者要确认服务端返回,就用fetch加keepalive: true。
结合CDN日志采集的完整接入实践
在CDN场景下,Beacon最常见的用途是把用户的访问质量数据(首屏时间、命中率、卡顿次数等)投递到日志收集端点,再由日志系统与CDN回源日志做关联分析。接入代码本身很简单,但有几个坑必须提前避开。
function reportToCDN(payload) {
var data = JSON.stringify(payload);
// 注意:sendBeacon对Content-Type有限制,常用text/plain规避预检
if (navigator.sendBeacon) {
var ok = navigator.sendBeacon('https://log.ipipp.com/collect', data);
if (!ok) {
fallback();
}
} else {
fallback();
}
function fallback() {
// 降级:用fetch keepalive兜底
fetch('https://log.ipipp.com/collect', {
method: 'POST',
body: data,
keepalive: true
}).catch(function () {});
}
}
// 在页面隐藏或卸载时上报
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'hidden') {
reportToCDN({ event: 'pageview', path: location.pathname });
}
});
window.addEventListener('pagehide', reportFlush); // pagehide比unload更可靠第一个坑是Content-Type。直接用sendBeacon发送JSON.stringify的结果时,默认的MIME类型是text/plain,这反而是好事,因为跨域场景下不会触发CORS预检。如果你用Blob强制指定application/json,浏览器会先发一个OPTIONS预检,而预检请求在页面卸载时同样可能被中断,等于白忙一场。推荐的做法是发送text/plain的JSON字符串,服务端按纯文本接收后自行解析。
第二个坑是大小限制。规范建议浏览器将信标数据限制在64KB以内,超出这个限制sendBeacon会直接返回false。所以不要试图把整份错误堆栈加上截图base64一起塞进去。正确的做法是在客户端做聚合和截断,把多条日志合并成一次批量上报,超长内容做摘要处理。
// 批量上报:本地攒够10条或页面隐藏时统一发送
var queue = [];
function track(event) {
queue.push({ event: event, ts: Date.now() });
if (queue.length >= 10) {
flush();
}
}
function flush() {
if (queue.length === 0) return;
var batch = queue.splice(0);
try {
navigator.sendBeacon('https://log.ipipp.com/batch',
JSON.stringify({ items: batch.slice(0, 20) })); // 截断保护
} catch (e) { /* 静默失败,统计类数据不重试 */ }
}最后一个建议:监听时机上优先使用pagehide和visibilitychange,而不是unload。移动端浏览器为了提升体验会大量忽略unload事件,尤其在bfcache(往返缓存)生效的场景下,visibilitychange变为hidden才是最后可靠的发送窗口。把上报逻辑挂在这两个事件上,配合sendBeacon的可靠投递特性,基本可以做到页面离开时数据零丢失,让CDN侧的质量监控真正跑起来。
CDNBeacon API信标发送修改时间:2026-09-04 17:58:48