首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >同样是发请求,为什么页面关闭时 fetch 是个废物?

同样是发请求,为什么页面关闭时 fetch 是个废物?

作者头像
前端达人
发布2026-06-30 16:42:53
发布2026-06-30 16:42:53
280
举报
文章被收录于专栏:前端达人前端达人

去年给一个后台系统做用户行为埋点,功能上线半个月后,数据分析的同事找来了——"你看,这个退出率的图怎么这么奇怪,感觉数据不全。"

我拉出服务器日志一对,果然:实际关闭页面的事件比收到的少了将近两成。数据在半路消失了。

当时的代码是这么写的:

代码语言:javascript
复制
window.addEventListener('beforeunload', () => {
  fetch('/api/track', {
    method: 'POST',
    body: JSON.stringify({ event: 'page_exit', ts: Date.now() })
  })
})

看起来没毛病,逻辑也对。但这里有一个很反直觉的事:浏览器关闭标签页时,正在运行的异步请求会被直接 kill 掉。

fetch 在页面关闭时为什么失效

fetch 是异步的。你调用它,它返回一个 Promise,整个流程建立在"页面还活着"这个前提上。

关标签页那一刻,浏览器要做的事情:停止所有 JS 执行、释放内存、断开网络连接。那个还没跑完的 fetch 请求,和未保存的 Word 文档一个待遇——直接没了。

代码语言:javascript
复制
页面关闭时的命运对比:

fetch 方式                          sendBeacon 方式
─────────                           ──────────────
用户关闭标签页                        用户关闭标签页
      │                                    │
      ▼                                    ▼
 浏览器开始卸载                      浏览器开始卸载
      │                                    │
      ▼                                    ▼
JS 执行停止                         数据已进入浏览器队列
      │                                    │
      ▼                                    ▼
fetch 连接被 kill                   浏览器后台完成发送
      │                                    │
      ▼                                    ▼
❌ 数据丢失                          ✅ 数据送达

sendBeacon:浏览器楼下那个信箱

navigator.sendBeacon() 是浏览器十多年前就提供的 API,专门为"临死前发消息"这个场景设计的。

它的逻辑不一样:你调用它,数据直接进浏览器的后台发送队列,然后你就走,浏览器自己负责把这封信投出去。不依赖 JS 继续运行,不要求页面还活着。

改写之后:

代码语言:javascript
复制
// JavaScript 版本
window.addEventListener('beforeunload', () => {
  const data = JSON.stringify({ event: 'page_exit', ts: Date.now() })
  navigator.sendBeacon('/api/track', data)
})
代码语言:javascript
复制
// TypeScript 版本
interface TrackPayload {
  event: string
  ts: number
  userId?: string
}

window.addEventListener('beforeunload', () => {
const payload: TrackPayload = {
    event: 'page_exit',
    ts: Date.now(),
    userId: getCurrentUserId()
  }
const success = navigator.sendBeacon('/api/track', JSON.stringify(payload))
if (!success) {
    console.warn('sendBeacon 入队失败,可能超出 64KB 限制')
  }
})

sendBeacon 返回一个布尔值——true 表示数据成功进入队列,false 表示入队失败(通常是超出大小限制)。注意,true 不代表服务器收到了,只是"投进信箱了"。

三个坑,踩过才知道

64KB 上限。 单次请求超过 64KB 直接返回 false,数据无声消失。有一次把完整的报错堆栈塞进去发监控,服务端半天没收到,排查了好久才发现是超重了。解法:分批发,或者大数据改走正常请求。

没法自定义 Header。 sendBeacon 默认 Content-Type: text/plain,改不了。如果后端要求 application/json,可以用 Blob 包一层指定类型,但这可能触发 CORS 预检请求。我们团队最后统一用 text/plain,后端自己解析 JSON 字符串,简单省事。

Token 没法带。 走不了 Authorization Header,需要鉴权的接口用不了。

批量发送比每次点击都发一条要好很多,服务端压力小,数据也更好处理。内存里攒够再发:

代码语言:javascript
复制
// JavaScript 版本
let buffer = []

document.addEventListener('click', (e) => {
  buffer.push({ type: 'click', x: e.clientX, y: e.clientY, ts: Date.now() })

  // 攒够 10 条,或者页面进后台了,立刻发出去
  if (buffer.length >= 10 || document.visibilityState === 'hidden') {
    navigator.sendBeacon('/api/interactions', JSON.stringify(buffer))
    buffer = []
  }
})
代码语言:javascript
复制
// TypeScript 版本
interface ClickEvent {
type: 'click'
  x: number
  y: number
  ts: number
}

let buffer: ClickEvent[] = []

document.addEventListener('click', (e: MouseEvent) => {
  buffer.push({ type: 'click', x: e.clientX, y: e.clientY, ts: Date.now() })

if (buffer.length >= 10 || document.visibilityState === 'hidden') {
    navigator.sendBeacon('/api/interactions', JSON.stringify(buffer))
    buffer = []
  }
})

这里用的是 visibilityState === 'hidden' 而不是 beforeunload,覆盖的场景更全——切换 Tab、锁屏、手机 App 切到后台都会触发,不只是关标签页。

如果你需要带 Token,用 fetch + keepalive

最近几年 fetch 加了 keepalive 参数,页面卸载时不会被 kill 掉:

代码语言:javascript
复制
fetch('/api/track', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${getToken()}`
  },
  body: JSON.stringify({ event: 'session_end', ts: Date.now() }),
  keepalive: true
})

可以带自定义 Header,也可以处理响应,适合需要鉴权的场景。同样受 64KB 约束,这点和 sendBeacon 一样。

选型的时候看这张图就够了:

代码语言:javascript
复制
你要发什么?
    │
    ├─ 简单埋点 / 错误上报 / 不需要鉴权
    │   └─ → sendBeacon  ✅ 轻量、稳定、零配置
    │
    ├─ 需要带 Token / 需要处理响应
    │   └─ → fetch + keepalive  ✅ 灵活,记得 64KB 限制
    │
    └─ 数据量大 / 需要投递确认
        └─ → WebSocket 或普通请求 + 重试机制

sendBeacon 是那种平时不会主动去查、但踩过坑之后会牢牢记住的 API。如果你的项目里有在 beforeunloadvisibilitychange 里发 fetch 的代码,值得检查一下——可能有 20% 的数据正在悄悄蒸发。

你们项目的埋点方案是怎么做的?

几个好奇的点:用的是自研方案还是三方 SDK(神策、GrowingIO 之类的)?有没有遇到过数据莫名少了一截、排查了半天才发现根本不是业务 bug 的情况?

评论区聊聊,说不定能帮到踩同样坑的人 👇

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-06-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • fetch 在页面关闭时为什么失效
  • sendBeacon:浏览器楼下那个信箱
  • 三个坑,踩过才知道
  • 如果你需要带 Token,用 fetch + keepalive
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档