首页/文章/javascript

navigator.sendBeacon全指南

2026-02-24
21268 分钟
...

在前端开发中,埋点系统是必不可少的一环。我们经常需要在用户关闭页面刷新跳转路由时,向服务器发送最后一条统计数据(比如用户停留时长、页面跳出率)。

但这看似简单的需求,在实现时却危机四伏:请求发不出去?页面跳转卡顿?今天我们就来聊聊这个问题的终极解决方案 —— navigator.sendBeacon

一、 痛点与传统方案的挣扎

场景还原

当用户点击关闭按钮时,浏览器会触发生命周期事件(unloadvisibilitychange)。如果我们直接使用普通的异步 AJAX (xhrfetch) 发送请求,浏览器通常会忽略它,因为页面都要销毁了,浏览器不想处理未完成的请求。

传统方案:同步 XHR

为了保证数据能发出去,以前的做法是将请求改为同步(Synchronous)

const syncReport = (url, { data = {}, headers = {} } = {}) => {
  const xhr = new XMLHttpRequest();
  // 第三个参数 false 表示同步请求
  xhr.open('POST', url, false);
  xhr.withCredentials = true;
  Object.keys(headers).forEach((key) => {
    xhr.setRequestHeader(key, headers[key]);
  });
  xhr.send(JSON.stringify(data));
};

致命缺陷

  1. 用户体验极差:同步请求会阻塞主线程。这意味着只有请求发送完成,页面才能关闭或跳转。在弱网环境下,用户会感觉页面“卡死”了。
  2. 浏览器废弃:现代浏览器(如 Chrome)已经明确表示将在页面卸载期间禁用同步 XHR,这种方法迟早失效。

二、 救世主:navigator.sendBeacon

1. 概念

navigator.sendBeacon() 是专门为“页面卸载时发送数据”而设计的 Web API。 它的核心能力是:将数据放入浏览器的发送队列,即使页面已经关闭,浏览器也会在后台默默完成发送。

2. 核心优势

  • 可靠性高:不受页面生命周期影响,确保数据不丢失。
  • 非阻塞:完全异步执行,不会阻塞页面关闭或跳转,用户体验丝滑。
  • 低优先级:浏览器会择机发送(通常是网络空闲时),不争抢关键资源。

3. API 语法

const result = navigator.sendBeacon(url, data);
  • url:请求地址。

  • data:要发送的数据,支持 ArrayBufferArrayBufferViewBlobDOMStringFormDataURLSearchParams

  • result(返回值):布尔值 (true / false)。

    • true:表示数据成功加入传输队列(注意:不代表服务器接收成功)。
    • false:表示队列已满,无法加入。

三、 实战:三种常见发送姿势

1. 发送普通字符串

默认 Content-Typetext/plain

const reportData = (url, data) => {
  // data 可能会被转为字符串 "[object Object]",建议先 stringify
  navigator.sendBeacon(url, JSON.stringify(data));
};

2. 发送 JSON 数据(推荐)

如果你希望后端接收到的 Content-Typeapplication/json 或者 application/x-www-form-urlencoded,需要使用 Blob 来手动指定。

const reportData = (url, data) => {
  // ✅ 正确写法:Blob 的第二个参数才是 options
  const blob = new Blob([JSON.stringify(data)], {
    type: 'application/json; charset=UTF-8' // 或者 application/x-www-form-urlencoded
  });
  navigator.sendBeacon(url, blob);
};

3. 发送 FormData

适用于需要上传文件或模拟表单提交的场景。浏览器会自动设置 Content-Typemultipart/form-data

const reportData = (url, data) => {
  const formData = new FormData();
  Object.keys(data).forEach((key) => {
    let value = data[key];
    // FormData 的 value 只能是字符串或 Blob
    if (typeof value !== 'string' && !(value instanceof Blob)) {
      value = JSON.stringify(value);
    }
    formData.append(key, value);
  });
  navigator.sendBeacon(url, formData);
};

四、跨域场景的“万能钥匙” —— 1px 像素图片

在某些场景下使用sendBeacon 会有跨域问题,而使用1px像素图片这种方式则利用了浏览器允许跨域加载资源(如图片、脚本)的特性,绕过了复杂的 CORS 配置

1. 核心原理

通过动态创建 Image 对象,将埋点数据通过 URL Query 的形式挂载在图片请求的地址后面。服务端在接收到请求后,记录日志并返回一个 1x1 像素的透明图片。

2. 代码实现

/**
 * 跨域埋点发送:1px 像素图片方案
 * @param {string} url - 接口地址
 * @param {Object} data - 埋点数据
 */
const reportByImg = (url, data) => {
  // 1. 构造查询参数字符串
  const params = Object.keys(data)
    .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`)
    .join('&');

  // 2. 创建图片实例
  const img = new Image();

  // 3. 监听回调(可选,用于监控发送是否成功)
  img.onload = () => console.log('埋点发送成功');
  img.onerror = (err) => console.error('埋点发送失败', err);

  // 4. 添加时间戳用于确保每次请求都被视为独立的新资源,避免被一些代理服务器或浏览器机制意外缓存
  const connector = url.includes('?') ? '&' : '?';
  img.src = `${url}${connector}${params}&_t=${Date.now()}`;
};

3. 方案对比:为什么不用其他方式?

特性1px ImageAjax (XHR/Fetch)sendBeacon
跨域限制(天然支持)严格(需服务端配置 CORS)较松(但仍受限于安全策略)
页面卸载可能被截断极易被截断可靠(异步非阻塞)
兼容性极好(所有浏览器)一般较好(现代浏览器)
承载数据量小(受 URL 长度限制)中(通常 < 64KB)

4.注意事项

  • URL 长度限制:由于数据是带在 URL 上的,浏览器对 URL 长度有限制(通常为 2KB-8KB)。如果数据量巨大,请拆分发送或改用 sendBeacon
  • GIF 是首选:服务端推荐返回 GIF 格式。对比 PNG 和 JPG,GIF 的透明像素块在文件头开销上是最小的(仅 43 字节)。
  • 内存释放:在一些极端高频埋点场景下,建议在 onload 之后执行 img = null 彻底释放内存。

五、 避坑指南(面试考点)

  1. 请求类型固定sendBeacon 只能发送 POST 请求。
  2. 无法读取响应:这是一个“射后不理”的 API,你无法获取服务器返回的数据(状态码、Response Body 等)。
  3. 数据大小限制:虽然标准没有明确规定,但浏览器对队列总大小有限制(通常在 64KB 左右),不适合发送大数据。
  4. Cookie 携带sendBeacon 默认会携带同域的 Cookie。

六、 面试模拟题

Q1:sendBeaconajax (XHR/Fetch) 有什么根本区别?

参考回答:

  • 生命周期:Ajax 请求属于页面上下文,页面关闭时请求会被取消(除非同步);sendBeacon 属于浏览器上下文,页面关闭后依然存活。
  • 交互体验:页面卸载时,同步 Ajax 会阻塞跳转;sendBeacon 是异步非阻塞的。
  • 功能限制sendBeacon 只能 POST,无法自定义 headers(除了 Content-Type),且无法读取响应。

Q2:如果浏览器不支持 sendBeacon 怎么办?

参考回答: 需要做降级处理。

  1. 检测 navigator.sendBeacon 是否存在。
  2. 如果不存在,降级为 同步 XHR 请求(虽然体验差,但得保数据)。
  3. 或者使用 <img> 标签发送 GET 请求(仅限数据量极小且不需要响应的场景)。

Q3:sendBeacon 返回 true 代表数据一定发送成功了吗?

参考回答: 不一定。返回 true 仅代表浏览器成功将数据加入了发送队列。如果网络断开、或者浏览器崩溃,数据依然可能发送失败。但相比于普通 Ajax,它的成功率已经高出了几个数量级。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
javascript2025-11-02
理解浏览器事件系统,从用户点击到事件对象的完整旅程
深入理解浏览器事件系统:从用户点击到事件对象的完整旅程 “当我点击页面按钮时,背后发生了什么?为什么回调函数能收到一个包含丰富信息的event对象?今天,让我们一起揭开浏览器事件系统的神秘面纱。” 一个令人困惑的现象 作为前端开发者,我们每天都在写这样的代码: 这段代码如此熟悉,以至于我们很少停下来思考:​ ​这个 对象到底从哪里来?它为什么能知道点击的精确坐标?为什么能识别是哪个元素被点击了?​ ​ 更神奇的是,当我们手动创建事件时:...
学习
javascript2025-10-01
实现大文件上传全流程详解
在日常开发中,大文件上传是个绕不开的坎——动辄几百 MB 甚至 GB 级的文件,直接上传不仅容易超时,还会让用户体验大打折扣。最近我用 Vue+Express 实现了一套完整的大文件上传方案,支持分片上传、断点续传、秒传和手动中。 一、先看效果:我们要实现什么? 先上核心功能清单,确保大家明确目标,知道我们要解决哪些实际问题: 大文件分片上传 :将文件切成固定大小的小片段分批上传,避免单次请求超时 秒传 :服务器已存在完整文件时,直接返...
学习
javascript2025-09-18
JavaScript 的多线程能力:Worker
如果你写过一些计算量稍大的 JavaScript 代码,比如图像处理、大量数据排序或者复杂的算法,你几乎肯定遇到过浏览器“卡死”的现象。点击页面没反应,动画也停了,就像整个世界都静止了。 这就是主线程被阻塞的典型后果。因为主线程既要负责执行 JavaScript,又要负责渲染页面、响应用户操作,一旦它被繁重的计算任务占满,就无暇顾及其他,用户体验便直线下降。 这个问题的根源,正是“主线程是单线程的”。那么,如何解决呢? 答案很简单:把这...
学习面试
javascript2025-09-15
一张 8K 海报差点把首屏拖垮
你给后台管理系统加了一个「企业风采」模块,运营同学一口气上传了 200 张 8K 宣传海报。首屏直接飙到 8.3 s,LCP 红得发紫。 老板一句「能不能像朋友圈那样滑到哪看到哪?」——于是你把懒加载重新翻出来折腾了一轮。 解决方案:三条技术路线,你全踩了一遍 1\. 最偷懒:原生 一行代码就能跑,浏览器帮你搞定。 🔍 关键决策点 2020 年后现代浏览器全覆盖,IE 全军覆没。 必须写死 ,否则 CLS 会抖成 PPT。 适用场景...
学习
javascript2025-09-10
🚀 Web Worker让你的应用丝滑
🌟 引言 在日常的前端开发中,你是否遇到过这样的困扰: 大数据处理时页面卡死 :处理几万条数据时,页面直接卡成PPT,用户点击毫无反应 复杂计算阻塞UI :图片处理、数据分析等计算密集型任务让整个应用假死 文件上传/下载卡顿 :大文件操作时,其他功能完全无法使用 实时数据处理性能差 :WebSocket接收大量数据时,页面渲染严重滞后 今天分享6个Web Worker的核心技巧,让你的应用告别卡顿,用户体验丝滑如德芙! 💡 核心技巧...
学习
javascript2025-09-10
手把手教你实现一个EventEmitter
你是不是也遇到过这种情况:代码里各种事件监听和触发,回调函数套了一层又一层,最后自己都理不清哪个事件先触发、哪个后触发了? 别担心,今天我就带你从零开始实现一个自己的事件触发器(EventEmitter),让你彻底掌握事件驱动的精髓,代码清晰度直接提升200%! 为什么要懂EventEmitter? 先说说为什么我们要关心这个。Node.js的核心就是事件驱动,比如文件读写、网络请求,都是通过事件来处理的。不会EventEmitter,...
学习面试

评论

请登录后发表评论

去登录
加载评论中...

目录