
引言“我们的埋点数据总是少一些尤其是用户快速关闭页面的时候。”这是上周一个做数据分析的同事向我抱怨的问题。他们的埋点方案很简单在页面卸载时beforeunload或unload事件中用fetch发送一个 POST 请求记录用户行为。但统计发现大约 15% 的数据丢失了——尤其是用户在移动端切换 App 或直接关闭浏览器标签时。为什么异步请求在页面关闭时会被取消有没有可靠的方式在页面卸载时发送数据答案是肯定的。今天我们就来聊聊两个专门为这种场景设计的 APInavigator.sendBeacon和fetch的keepalive选项。一、问题的根源页面卸载与网络请求的生命周期当我们用常规的fetch或XMLHttpRequest发送请求时这些请求默认是与当前页面绑定的。如果页面正在卸载unload浏览器可能会终止这些尚未完成的网络请求以释放资源或加快卸载过程。即使在beforeunload事件中发起同步请求如xhr.open(POST, url, false)虽然能确保请求发出但会阻塞页面卸载严重影响用户体验浏览器会弹出确认框并且页面会卡死。现代浏览器甚至可能限制或废弃同步 XHR。因此我们需要一种不阻塞页面卸载且浏览器会尽力发送的请求方式。二、第一把利器navigator.sendBeaconnavigator.sendBeacon()是专门为解决这个问题而设计的。它在 2014 年随 Beacon API 引入旨在可靠地、异步地发送少量数据且不会延迟页面卸载。2.1 基本用法window.addEventListener(unload,(event){constdataJSON.stringify({event:page_exit,time:Date.now()});navigator.sendBeacon(/log,data);});sendBeacon接收两个参数URL数据发送的地址。data可选要发送的数据可以是ArrayBuffer、TypedArray、DataView、Blob、string、FormData或URLSearchParams对象。它返回一个布尔值true表示浏览器已经将请求加入发送队列但不保证成功到达服务器false表示队列已满或其他原因导致请求无法加入。2.2 特点与限制方法固定为 POST无法更改。数据大小有限制一般建议不超过 64KB不同浏览器有差异Chrome 是 64KB。可靠但不保证浏览器会尽力在页面卸载后发送但如果用户离线或网络中断请求可能丢失。低优先级Beacon 请求的优先级较低不会与关键资源竞争带宽。不会阻塞页面即使数据量较大也不会影响页面关闭速度。2.3 适用场景用户行为埋点如点击、停留时长、页面退出。错误日志上报。任何需要在页面卸载时发送的“一锤子买卖”数据。三、第二把利器fetch 的 keepalive 选项fetchAPI 在较新的浏览器中提供了一个keepalive选项专门用于在页面卸载时保持请求存活。它的功能与sendBeacon类似但更灵活。3.1 基本用法window.addEventListener(unload,(){fetch(/log,{method:POST,body:JSON.stringify({event:page_exit,time:Date.now()}),headers:{Content-Type:application/json},keepalive:true});});只需要在fetch的配置对象中加上keepalive: true。3.2 特点与限制可以使用任何 HTTP 方法GET、POST、PUT 等。可以自定义请求头但受限于 CORS 策略如果是跨域。数据大小限制与sendBeacon类似总请求大小通常限制在 64KB 以内包括 URL 和头部。不支持流式响应因为页面可能已经关闭无法处理响应。fetch返回的 Promise 在页面卸载后可能永远不会 resolve/reject。可能被浏览器延迟发送在极端情况下如果请求排队过长或网络差可能被丢弃。3.3 与 sendBeacon 的对比特性sendBeaconfetch keepalive方法仅 POST任意 HTTP 方法自定义头部有限受浏览器控制完全自定义但受 CORS 限制请求体类型支持多种类型支持多种类型同 fetch数据大小约 64KB约 64KB响应处理无法获取无法获取Promise 可能永不完成浏览器支持广泛IE 不支持较新Chrome 66、Firefox 68、Safari 16适用性简单的单次上报需要自定义方法/头部的上报四、深入原理keepalive 如何工作当我们在常规的fetch中设置keepalive: true时浏览器会将这个请求标记为“独立于文档的生命周期”。这意味着即使发起请求的页面被卸载请求也不会被取消。浏览器会将请求的控制权转移到后台继续发送。请求的资源消耗不计入当前页面的资源限制而是计入全局的 Beacon 配额。请求不能保证完成如果浏览器进程被强制终止或网络断开数据依然会丢失。sendBeacon的内部实现也是类似的只是 API 更简洁且默认使用 POST。五、实战陷阱与最佳实践5.1 不要在 beforeunload 中做复杂操作beforeunload中只能做轻量操作不要执行耗时的同步任务。sendBeacon和keepalivefetch 都是异步的不会阻塞所以是安全的。5.2 小心跨域限制如果目标 URL 与当前页面不同源fetch keepalive会受到 CORS 限制需要服务器支持适当的 CORS 头。sendBeacon同样受 CORS 限制但因为是 POST 方法需要服务器处理预检请求吗实际上sendBeacon发送的请求是简单请求Content-Type 只能是text/plain、application/x-www-form-urlencoded或multipart/form-data所以不会触发预检。如果需要发送 JSON需要特别注意直接传 JSON 字符串时Content-Type 会被设为text/plain;charsetUTF-8服务器需要相应处理。5.3 数据大小不要超过限制尽量控制上报数据在几千字节内。如果需要发送大量数据可以考虑在页面可见时批量发送或者使用 Service Worker 进行离线存储待页面恢复后再发送。5.4 不要依赖响应结果因为页面可能已经关闭你无法获得响应。如果必须确认数据到达可以改用普通请求在页面可见时发送或者设计一种去重机制。5.5 何时用 sendBeacon何时用 fetch keepalive如果你只需要简单的 POST 上报且数据量小优先用sendBeacon兼容性更好语法简单。如果需要自定义请求方法如 PUT或自定义头部如携带认证 token或者要发送的数据格式要求特殊 Content-Type就用fetch keepalive。5.6 备选方案Service WorkerService Worker 可以拦截 fetch 事件即使页面关闭也能在后台处理请求如果浏览器支持后台同步。但配置较复杂适合需要离线能力的高级场景。六、常见问题解答Q1sendBeacon 能保证数据一定送达吗不能。它只是“尽力而为”。如果用户设备断网、浏览器崩溃或强制关机数据依然会丢失。但对于大多数正常关闭页面的场景可靠性远高于普通异步请求。Q2为什么我的 keepalive fetch 在 Chrome 中报错“Failed to fetch”可能原因跨域且服务器未正确配置 CORS。请求体超过大小限制Chrome 64KB。页面卸载太快浏览器来不及将请求加入队列罕见。Q3keepalive fetch 的 Promise 会怎样Promise 可能会一直处于 pending 状态永远不会 resolve 或 reject因为页面上下文已销毁。所以不要对其使用await或.then除非你能确保页面不会立即卸载。Q4如何在移动端 Safari 上使用Safari 从 16.0 开始支持fetch keepalive之前的版本只能使用sendBeaconSafari 12.1 支持。可以写一个 fallbackfunctionsendOnUnload(url,data){if(navigator.sendBeacon){navigator.sendBeacon(url,data);}elseif(fetchRequest.prototype.hasOwnProperty(keepalive)){fetch(url,{method:POST,body:data,keepalive:true});}else{// 最后的后备同步 XHR不推荐但总比丢失好constxhrnewXMLHttpRequest();xhr.open(POST,url,false);xhr.send(data);}}七、总结页面卸载时的数据上报一直是前端埋点的一个痛点。sendBeacon和fetch keepalive为这个场景提供了标准、可靠的解决方案。理解它们的原理和限制能让你在设计分析系统时更加从容。下次遇到“关闭页面后数据丢失”的问题不要再尝试同步 XHR 或setTimeout拖延了——用上这些现代 API既保护用户体验又提升数据完整性。最后留一道思考题假设你的页面需要在上报数据的同时从服务器获取一个“再见”消息比如显示一个弹窗应该怎么做可以用 keepalive fetch 吗为什么答案下期揭晓也欢迎在评论区讨论每日一问你在项目中遇到过页面卸载时数据丢失的情况吗是如何解决的分享你的经验大家一起学习