news 2026/9/30 13:16:54

页面关闭前埋点丢失:sendBeacon验收

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
页面关闭前埋点丢失:sendBeacon验收

直答:页面关闭时浏览器会取消未完成请求,XHR 必丢。sendBeacon 交给后台排队更可靠,但只保证发出,不保证到达。

最后一批访客的停留时长全是 1~2 秒——不是用户真的划走了,是关闭页面前那一刻的「离开事件」根本没发出去。服务端只收到了进来的事件,没收到离开的事件,停留时长就被截断在最后一次活动上。

不写长篇大论,下面给一份可以直接复制到团队 Wiki 里的验收清单。

一、为什么 XHR 在页面关闭时一定会丢

浏览器在卸载页面时会做一件事:取消所有还没完成的网络请求。你在beforeunload里发一个 XHR,请求刚构造出来,页面已经被关了,请求被浏览器掐断。这不是网络问题,也不是 SDK bug,是浏览器故意的——它不想让一个即将销毁的页面继续占用网络。

历史上大家的做法是在页面里塞一个 1×1 像素的图片,把埋点参数拼在 src 上。图片请求是同步的,浏览器会等它发出去再关页面。但这种做法在 HTTPS、跨域、大数据包时都不稳定,现在已经被 W3C 的 Beacon API 正式取代。

要理解这件事,得先把页面关闭时的事件顺序理清楚。现代浏览器在页面离开时会依次触发 beforeunload、pagehide、unload 三个事件。beforeunload 原本用来弹「确定离开吗」的确认框,但 Chrome 117 之后对这类弹窗加了严格限制,移动端浏览器更是经常直接不触发它;unload 则在页面真正卸载时触发,可一旦走到这里,页面资源随时可能被回收,再发请求已经太晚。pagehide 夹在中间,它在页面被隐藏、进入往返缓存(bfcache)、或者被关闭时都会触发,时机更早、覆盖面更全,也是 web.dev 官方推荐用来做离开上报的事件。把上报绑在 pagehide 上,等于在浏览器还愿意配合的时候就把数据交出去。

二、sendBeacon 怎么写

document.addEventListener('pagehide', function () { const payload = JSON.stringify({ event: 'page_hide', path: location.pathname, ts: Date.now() }); if (navigator.sendBeacon) { navigator.sendBeacon('/logcount.html', new Blob([payload], { type: 'text/plain' })); } else { // 老浏览器降级方案 new Image().src = '/logcount.gif?data=' + encodeURIComponent(payload); } });

几个要点:

  • 绑pagehide而不是beforeunload——后者在现代浏览器里已经不可靠,Chrome 117 起对它的弹窗都做了限制;
  • body 用 Blob 包一下,Content-Type 设成 text/plain 可以避开 CORS 预检;
  • sendBeacon 只支持 POST,不能自定义 Header,鉴权 token 要拼在 URL 或 body 里;
  • 单个 beacon 的 body 不要超过 64KB,超过会被浏览器静默丢弃。

这段代码里有几个选择需要解释一下。用 pagehide 而不是 unload,是因为 unload 触发得太晚,浏览器已经在回收资源;用 new Image() 兜底而不是继续用 XHR,是因为图片请求会被浏览器在卸载时尽量发完,这是老时代的兼容手段。Blob 的 type 写成 text/plain 是一个取巧的办法:这种简单内容类型不会触发跨域预检请求,beacon 能直接发出去,服务端再自己解析 body 里的 JSON。

实际项目里不会每次都手写 Blob,通常会封装一个统一的上报函数,把 JSON 序列化、类型声明、降级逻辑都收进去,业务侧只关心传什么事件。下面这个封装做了三件事:先把数据序列化成 JSON 并用 Blob 声明类型,优先走 sendBeacon,不支持时退回 fetch 的 keepalive,最后再退到图片像素。

function reportBeacon(url, data) { const payload = new Blob( [JSON.stringify(data)], { type: 'application/json' } ); if (navigator.sendBeacon && navigator.sendBeacon(url, payload)) { return; // 浏览器已接受后台发送 } if (window.fetch) { // 降级:fetch 开启 keepalive,允许页面卸载后继续发送 fetch(url, { method: 'POST', keepalive: true, body: payload }); return; } // 最后兜底:图片像素(不推荐作为主路径) new Image().src = url + '?d=' + encodeURIComponent(JSON.stringify(data)); } document.addEventListener('pagehide', function () { reportBeacon('/logcount.html', { event: 'page_hide', path: location.pathname, ts: Date.now() }); });

这里有两个细节容易踩。一是 Blob 的 type 要显式写成 application/json,服务端才能按 JSON 解析;如果留空或写成 text/plain,后端的 JSON 中间件可能直接报错。二是 sendBeacon 的返回值只是「浏览器接不接受这个任务」,true 不代表服务端收到了,所以关键数据不要只靠它,用户真正点按钮、下单这种动作发生时就该立刻发,而不是攒到关闭时才交。

二·补:错误写法与正确写法对照

把团队里常见的写法摆在一起对照,问题会更直观。下面这张表左边是线上常见的错误写法,右边是修正后的写法,每一行都是真实排查时反复出现的坑。

错误写法问题正确写法
在 unload 里发 XHR页面已开始销毁,请求被取消绑 pagehide,用 sendBeacon
fetch 不带 keepalive卸载时 fetch 同样被掐fetch 加 keepalive:true
直接传字符串当 bodyContent-Type 不对,后端解析失败用 Blob 声明 application/json
只在 beforeunload 上报移动端常不触发,数据大量丢失pagehide 为主,beforeunload 兜底
一次 beacon 塞整屏数据超过体积上限被静默丢弃拆包,只发关键字段

这张表可以直接贴进 code review 的 checklist。新人最容易犯的不是不知道 sendBeacon,而是知道了却把它绑在 unload 上、或者忘了用 Blob 包 JSON,结果数据照样丢,还以为是 API 本身不可靠。

(如下图)

关闭页埋点的三段验收路径

三、三段验收清单

写完代码不算完,按下面三张清单逐项打勾,任何一段漏了都不能上线。

三段的意思是:客户端发出去不等于服务端收到,服务端收到不等于数据真的进了分析库。很多团队只看 DevTools 里那条请求变绿就以为万事大吉,结果过两天发现看板上根本没有对应事件——问题出在服务端解析失败或者入库管道断了,而客户端那一端是绿的。所以三段必须分别验收,任何一段漏了都不能上线。

第一段:客户端「真的发出去了」

这一段的具体操作是这样的:打开 Chrome DevTools 切到 Network 面板,先勾选面板上方的 Preserve log(保留日志)——这一步最关键,默认情况下页面一关闭,请求列表会被清空,你根本看不到关闭瞬间发出去的那条请求。接着正常操作页面,再通过关标签页、点外链或点浏览器返回触发离开,回到 Network 面板过滤你的打点域名。这时应该能看到一条 POST 请求,发起类型(Initiator)里能看到 beacon 相关字样。点开这条请求,确认 Status 是 200 或 (pending) 而不是 (canceled),再切到 Payload 标签核对 body 里的事件名、页面路径和时间戳是否完整。如果看到的是 (canceled),说明上报还在被浏览器取消,多半是事件绑错了或者没用 sendBeacon。

  • Chrome DevTools Network 过滤埋点域名,关闭页面时能看到一条 POST 请求
  • 请求状态是 (pending) 或 200,不是 (canceled)
  • 请求 body 完整,没有被截断
  • 切换到移动端 Safari 复测一遍(iOS 的限制和 Chrome 不同)
  • 断网状态下关闭页面,请求不应该阻塞 UI

第二段:服务端「真的收到了」

  • 服务端 access log 里能看到对应的 POST 请求记录
  • 请求到达时间和客户端关闭时间差在 1 秒内
  • 跨域 OPTIONS 预检没有被触发(beacon 用 text/plain 本应免预检)
  • 用 curl 模拟 beacon 请求,服务端能正常解析 body

第三段:数据「真的落库了」

  • 后台事件列表里能查到这条 page_hide 事件
  • 把同一时间窗口的 page_view 和 page_hide 配对,配对率应在 90% 以上
  • 停留时长分布不再集中在 1~2 秒这种异常区间
  • 观察 24 小时,关闭事件量与 PV 比例稳定,不突增突降

(如下图)

三段验收各自的检查项与通过标准

四、beacon 也救不了的场景

sendBeacon 不是万能的。下面这些情况它照样丢:

场景会丢吗对策
用户正常关闭标签页基本不丢靠 pagehide + beacon
用户切换到后台一段时间可能丢visibilitychange 也打一次
用户直接杀浏览器进程会丢关键事件在用户操作时立即发,不要等关闭
断网状态关闭会丢本地队列 + 下次启动补发
请求 body 超过 64KB会被静默丢弃拆包或只发关键字段

还有一个移动端特有的场景值得单独说。用户在手机上把浏览器切到后台、或者直接切到别的 App,这时候页面并没有被关闭,pagehide 不一定触发,但人其实已经走了。这种情况下建议同时监听 visibilitychange,当 document.visibilityState 变成 hidden 时再补一次离开上报。否则移动用户的停留时长会被算得偏长——系统以为他还在看,其实早就切走了。把 pagehide 和 visibilitychange 两个事件都绑上同一份上报逻辑,移动端的数据才不会虚高。

落到具体的统计工具上,主流 Web JS SDK 一般都把常规浏览和事件上报做了队列缓冲,自定义事件通过全局队列 _yhxw456_trackdata.push(['event', 分类, 名称, 属性]) 上报。开发者在卸载时刻补充的离开、停留时长事件,可以在 SDK 常规上报之外,再用上面这套 pagehide 加 sendBeacon 的逻辑兜底发一份,两者配合,关闭瞬间的流失才不会成为盲区。在工具侧,事件分析与实时流量监控可看到 page_view 和 page_hide 的配对率;要按「丢失率」这种维度做长期下钻,需要按维度拆分事件,这部分在专业版及以上更顺手。具体档位与配额以官网定价页为准。

常见问题

Q1:为什么页面关闭时 XHR 埋点会丢?
A:页面卸载时浏览器会取消未完成的请求,XHR/fetch 在 beforeunload 里发出往往发不出去。这是浏览器行为,不是网络问题。

Q2:sendBeacon 有什么限制?
A:只支持 POST,body 大小通常限制在 64KB 以内,不能自定义请求头里的鉴权字段,且是异步、不等响应的。

Q3:为什么不建议用 beforeunload?
A:现代浏览器已逐步弃用 beforeunload,改用 pagehide。Chrome 从 117 起对 beforeunload 弹窗做了限制,埋点也建议绑到 pagehide。

Q4:sendBeacon 一定能发出去吗?
A:不能。它由浏览器保证在后台排队,但如果用户直接杀进程或断网,照样会丢。所以关键路径埋点不能只靠 beacon,要在用户操作时就发。

Q5:怎么验收 beacon 真的到了服务端?
A:三段验收:DevTools Network 看到请求、服务端 access log 收到、数据库落库。只看客户端发出去不算数。

总结

关闭页埋点的核心不是「用什么 API」,而是「怎么证明它真的到了」。把发送、到达、入库这三段验收清单贴进上线 Checklist,每次接新埋点都过一遍,停留时长这种敏感指标就不会再莫名短一截。绑事件选 pagehide 不选 unload,发数据选 sendBeacon 不选裸 XHR,调试时记得勾 Preserve log——这三件事做到位,剩下的只是把它固化成团队规范。工具能帮你看数据看板,但这三关必须自己盯;发送、到达、入库任何一段断了,看板上的漂亮数字都站不住。

数据来源:

  1. W3C Beacon API 规范
  2. MDN Web Docs《navigator.sendBeacon》
  3. MDN Web Docs《Page Visibility API》
  4. MDN Web Docs《beforeunload 事件》
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 13:15:05

Claude Code接入本地Qwen:macOS离线编程搭建全攻略

最近把macOS上的开发环境彻底折腾了一遍,核心目标就一个:让 Claude Code 跑在本地大模型上,模型用 Qwen,所有请求全程不出这台电脑。这套方案我实际用了一个多月,日常写脚本、重构单文件、改 bug、补注释,完…

作者头像 李华
网站建设 2026/9/30 13:14:25

文件系统与跨平台适配:从inode到VFS核心原理与排查

“文件系统”这四个字,我见过太多人把它当成“背概念”的内容对待了——文件系统是什么、有哪些类型、FAT32和NTFS有什么区别,考试能写出来,但真到工作上,U盘在Windows和Linux之间来回拷数据变成一堆乱码,服务器重启后…

作者头像 李华
网站建设 2026/9/30 13:13:43

Codex接入Jev模型后端:从密钥申请到报错排查全攻略

作为一个常年跟各种模型工具打交道的人,我必须先泼一盆冷水:别再执着于把Codex跟单一模型绑死了。我这段时间把Jev模型接到Codex里实跑了一周,体感确实像换了台新机器。这篇东西不写虚的,就把我怎么从官网申请密钥、怎么改配置、怎…

作者头像 李华
网站建设 2026/9/30 13:13:43

Jev 能不能玩 Overcooked?从配置到联机的完整判断指南

1. 从一个看似无厘头的问题说起“Jev 能不能玩 Overcooked?”——第一次看到这个问题,我愣了三秒。Jev 是谁?Overcooked 又是什么?如果你恰好两个都熟,那大概率会心一笑;如果你只熟一个,那这篇内…

作者头像 李华
网站建设 2026/9/30 13:12:09

OpenGame架构深度解析:CLI、Core与工具系统如何协同工作?

OpenGame架构深度解析:CLI、Core与工具系统如何协同工作? 【免费下载链接】OpenGame OpenGame: Open Agentic Coding for Games 项目地址: https://gitcode.com/gh_mirrors/op/OpenGame OpenGame 是一个面向终端的开源游戏 Agent 框架&#xff0c…

作者头像 李华
网站建设 2026/9/30 13:11:56

AI桌面换装视频全流程拆解:从图像生成到图生视频实操指南

最近刷短视频,一定见过这类"AI桌面换装视频":一个人坐在电脑前,桌面上是熟悉的耳机、水杯、显示器,随着音乐卡点,身上的衣服一套接一套换——从家居服到西装,从汉服到运动装,动作还特…

作者头像 李华