页面关闭时怎样可靠上报?sendBeacon 和 keepalive 怎么选?
在 visibilitychange 变为 hidden 时上报,而不是依赖 unload/beforeunload;sendBeacon 只发 POST、只返回是否入队;fetch keepalive 可自定义请求并读响应;fetchLater 由浏览器在页面离开或超时时代发,目前仅 Chromium 支持。
参考答案
简答
- 上报时机选
visibilitychange且visibilityState === 'hidden';unload、beforeunload在移动端常常不触发,unload还会妨碍 bfcache。 sendBeacon与fetch的keepalive共用 64 KiB 配额,超额时互相回退也会失败;要平时分批 flush、控制单次体积。两者的细节见《sendBeacon 返回 true 就代表上报成功了吗?页面离开时怎样发送统计?》。fetchLater让浏览器在页面离开或activateAfter超时后代为发送,适合"先登记、最后再发",但仍是实验性 API,目前只有 Chromium 系浏览器支持,必须做特性检测和降级。
上报时机
JavaScript
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') flush();
});
用户切到其他应用后再从任务管理器杀掉浏览器时,unload 根本没有机会执行,而 visibilitychange 在切走时就已触发。hidden 不一定意味着离开,用户可能回来,所以 flush 只发送当前缓冲的数据,后续数据继续累积。
pagehide 与 bfcache 兼容,但可靠性问题和 unload 类似,MDN 推荐优先使用 visibilitychange。bfcache 的细节见《bfcache 与 HTTP 缓存有什么区别?》。
三种方式怎么选
| 方式 | 方法 | 读响应 | 体积限制 | 兼容性 |
|---|---|---|---|---|
sendBeacon | 仅 POST | 否 | 与 keepalive 共享 64 KiB | 广泛支持 |
fetch + keepalive | 任意 | 可以 | 与 sendBeacon 共享 64 KiB | 广泛支持 |
fetchLater | 任意 | 否 | 有配额,超出抛 QuotaExceededError | 仅 Chromium |
sendBeacon 返回 false 多半是配额已满,这时改用 keepalive 同样会失败。可靠做法是平时按数量或时间分批 flush,离开时只发剩余的小块数据;keepalive 只作为不支持 sendBeacon 环境的回退。返回值、Content-Type 与预检的细节见《sendBeacon 返回 true 就代表上报成功了吗?页面离开时怎样发送统计?》。
fetchLater
JavaScript
let controller;
function schedule(data) {
if (!('fetchLater' in window)) return; // 降级到 visibilitychange + sendBeacon
controller?.abort(); // 数据更新时取消旧请求再重新登记
controller = new AbortController();
try {
fetchLater('/collect', {
method: 'POST',
body: JSON.stringify(data),
signal: controller.signal,
activateAfter: 60_000,
});
} catch (error) {
if (error.name !== 'QuotaExceededError') throw error;
}
}
它的价值在于无需抓准离开时机:页面被销毁或进入 bfcache 时,浏览器会发出已登记的请求,不依赖离开事件是否触发。限制包括:只接受 HTTPS 等可信 URL,请求体不能是流,响应会被丢弃,受 CSP connect-src 约束。
常见追问
- 为什么不在
beforeunload里用同步 XHR?同步请求阻塞页面离开,Chrome 等浏览器已禁止在页面卸载期间发送同步 XHR,体验和可靠性都差。 - 数据会不会重复?
hidden可能触发多次,用事件 ID 去重,服务端也要能处理重复。 - 上报接口要带 Cookie 吗?尽量用 URL 参数或请求体里的匿名 ID,减少跨站 Cookie 带来的限制。