当前位置:首页 > 文章列表 > 文章 > 前端 > Fetch keepalive 处理页面卸载前的小请求

Fetch keepalive 处理页面卸载前的小请求

来源:17golang原创 2026-09-29 03:56:30 0浏览 收藏

我第一次处理“用户离开页面前补发一条状态”的需求时,直觉上只是给普通 fetch() 套一个 beforeunload。桌面测试看起来没问题,换到移动端、快速切换应用和浏览器返回缓存后,缺失率却很难解释。更稳妥的方向不是阻塞页面离开,而是把小请求声明为 keepalive,并在文档进入隐藏状态时尽早发起。

核心写法:监听 visibilitychange,当 document.visibilityState === "hidden" 时调用 fetch(url, { keepalive: true })。请求体必须保持很小,官方资料给出的 keepalive 请求体限制是 64 KiB;服务端还要用事件 ID 做幂等,因为页面生命周期末端的发送不适合依赖前端等待响应后再更新状态。

官方文档:https://developer.mozilla.org/en-US/docs/Web/API/RequestInit#keepalive

趋势信号:页面末端通信从“阻塞离开”转向生命周期感知

早期方案常在 unload 中发同步 XHR,或者用循环拖延页面关闭。它们会损害下一次导航体验,而且 unload/beforeunload 在移动设备上并不可靠,还可能影响浏览器的往返缓存(bfcache)。现在更合理的做法,是在页面变为 hidden 时尽早提交小型数据,并让浏览器负责在文档销毁后继续承载 keepalive 请求。

这里的“继续承载”不是无限后台任务。它只适合小型分析、诊断、会话摘要、轻量草稿状态和一次性业务事件,不适合图片、日志包或大 JSON 上传。把 keepalive 理解成生命周期边界上的小通道,比把它当作通用后台传输更准确。

解决的问题:让请求脱离页面销毁边界

RequestInit.keepalive 默认是 false。设置为 true 后,即使发起请求的页面在请求完成前被卸载,浏览器也不会仅因为页面卸载而中止该请求。Fetch 仍然返回 Promise,并允许设置方法、请求头等属性;不过页面已经销毁时,业务代码不能假设自己还一定有机会处理 Promise 的结果。

页面生命周期、fetch keepalive、浏览器请求队列与服务端事件入口的关系图
图1:页面生命周期、Fetch 配置与请求承载边界的静态关系;这是结构说明图,不是浏览器截图。

对我来说,最大的认知变化是:不要把“什么时候触发”和“请求能否继续”混成一个问题。visibilitychange 负责提供更合适的发送时机,keepalive 负责声明页面离开后仍希望完成传输;两者缺一不可。

采用路径:visibilitychange + 一次性提交

下面是一个可直接改造的最小实现。它做了三件事:只在 hidden 状态触发;用布尔值避免同一页面生命周期重复提交;为每个事件生成稳定 ID,便于服务端去重。

let submitted = false;

function buildExitEvent() {
  return {
    event_id: crypto.randomUUID(), // 服务端用它执行幂等去重
    type: "page_hidden",
    path: location.pathname,
    occurred_at: new Date().toISOString(),
  };
}

function submitExitEvent() {
  if (submitted) return; // 防止 visibility 状态抖动造成重复提交

  const body = JSON.stringify(buildExitEvent());
  const bytes = new Blob([body]).size;

  if (bytes >= 64 * 1024) {
    console.warn("keepalive payload is too large"); // 超限数据应提前拆分或缩减
    return;
  }

  submitted = true;
  void fetch("/events", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body,
    keepalive: true, // 页面卸载后仍允许浏览器继续发送此小请求
    credentials: "same-origin",
  }).catch((error) => {
    console.debug("exit event was not delivered", error); // 页面仍存活时记录失败信息
  });
}

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    submitExitEvent(); // 比依赖 unload 更早地提交
  }
});

示例使用同源 /events,这样不需要额外处理跨域许可。若必须跨域,仍要满足普通 Fetch 的 CORS 和凭据规则。不要为了“页面快关了”就绕过权限模型,也不要把敏感凭据塞进请求体。

受益角色:哪些小请求适合 keepalive

场景为什么适合仍需注意
页面停留摘要数据小,离开前自然形成最终状态服务端按 event_id 去重
错误诊断尾事件可补充最后一个页面状态不能替代持续日志上报
轻量草稿标记只提交版本号或短字段真正内容应提前保存
任务完成回执请求短、语义单一关键业务必须有服务端确认机制

我不会把支付确认、权限变更、库存扣减这类关键动作只放在页面卸载时。keepalive 能降低“页面走了,请求就被中断”的概率,但页面事件本身仍可能因为进程终止、系统回收或网络断开而没有机会发出。关键业务应在用户动作发生时立即提交,并有可恢复的服务端状态。

风险一:64 KiB 是硬边界,不是推荐目标

MDN 明确写出 keepalive 请求体限制为 64 KiB。实际工程不应该把 63.9 KiB 当成安全设计,因为序列化内容可能增长,同一时刻还可能存在其他待发送请求。更好的策略是只保留必要字段,将详细日志在页面存活阶段分批发送。

function compactSession(session) {
  return {
    event_id: session.eventId,
    duration_ms: Math.round(session.durationMs),
    last_action: session.lastAction,
    error_count: session.errors.length, // 只发送计数,不携带完整错误数组
  };
}

function canUseKeepalive(payload, softLimit = 48 * 1024) {
  const body = JSON.stringify(payload);
  return new Blob([body]).size 

也不要给 keepalive 请求使用流式 ReadableStream 请求体。页面末端的小数据应先完成序列化,得到可计算长度的字符串、Blob 或其他定长 BodyInit。

风险二:页面事件和网络传输都不是业务事务

一个容易忽略的代价是重复。页面可能先进入 hidden,随后又恢复;路由和组件也可能各自注册监听器。服务端如果直接累加,就会把一次会话算成多次。我的做法是让每条事件携带 event_id,数据库以唯一键或幂等表拒绝重复。

async function handleEvent(request, store) {
  const event = await request.json();

  if (!event.event_id) {
    return new Response("missing event_id", { status: 400 }); // 拒绝不可去重的事件
  }

  const inserted = await store.insertOnce(event.event_id, event);
  return new Response(null, {
    status: inserted ? 202 : 204, // 重复事件返回成功语义,避免客户端无效重试
  });
}

这是示意性的服务端接口,insertOnce 需要由数据库唯一约束或原子写实现。不要用“先查询、再插入”的非原子组合冒充幂等。

选择边界:什么时候用 keepalive,什么时候用 sendBeacon

两者都面向小数据,但能力并不相同。sendBeacon() 天生面向异步 POST,不提供读取响应的 Fetch Promise;当需求只是“把一小段分析数据交给服务端”时,它通常更简单。需要其他 HTTP 方法、自定义请求属性,或者页面仍存活时希望读取响应时,fetch(..., { keepalive: true }) 更灵活。

fetch keepalive 与 sendBeacon 在方法、请求属性、响应和体积限制上的关系图
图2:Fetch keepalive 与 sendBeacon 的能力和限制对照;这是静态说明图,不是运行结果。
判断项fetch keepalivesendBeacon
HTTP 方法可按 Fetch 规则设置POST
请求属性可设置 headers、credentials 等接口更简化
响应处理返回 Promise;页面仍存活时可处理不提供响应读取
典型用途需要更多请求控制的小事件简单分析与诊断数据
数据规模keepalive 请求体受 64 KiB 限制排队数据同样只适合小体积

观察指标:上线后不要只看前端 Promise

页面销毁后,前端未必还能记录 Promise 最终状态,因此效果评估要以后端为准。我会至少观察:

  • 客户端生成事件数与服务端接收事件数的差值;
  • 按 event_id 统计的重复率;
  • 请求体超过软上限而被前端放弃的次数;
  • 不同浏览器、移动端与桌面端的接收率差异;
  • 接口状态码、处理延迟和服务端幂等冲突次数。

这里要保持中立:keepalive 解决的是“页面卸载不应自动中止请求”这一层问题,并不保证任意环境下百分之百送达。对允许少量丢失、可去重、数据很小的尾事件,它很合适;对必须确认成功的关键业务,它只能是辅助通道。

相关问题

keepalive 和 HTTP Keep-Alive 是一回事吗?

不是。Fetch 的 keepalive 描述请求是否允许在发起页面卸载后继续;HTTP Keep-Alive 讨论的是连接复用和持续连接,它们处于不同层次。

为什么不直接监听 unload?

unload/beforeunload 在移动端等场景可能不触发,还可能影响 bfcache。更推荐在 visibilitychange 进入 hidden 时提交,必要时把 pagehide 作为兼容补充。

keepalive 能上传文件吗?

不适合。请求体受 64 KiB 限制,设计目标就是小型尾请求。文件上传应在页面存活阶段使用正常上传、分片或可恢复传输。

页面关闭后还能依赖 fetch 的响应吗?

Fetch 本身返回 Promise,但页面销毁后不能假设脚本环境仍能处理结果。需要强确认的业务应在用户操作时完成,并由服务端状态驱动恢复。

我的最终取舍很简单:普通小型 POST 且不关心响应,就先看 sendBeacon;需要 Fetch 的方法、请求属性或响应能力,就用 keepalive。无论选哪一个,都把触发点放在更可靠的页面生命周期事件上,把体积控制和幂等放在设计里,而不是等丢数据后再补救。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
米坛社区工具下载入口怎么找?AstroBox、表盘自定义工具与 Notify 区分说明米坛社区工具下载入口怎么找?AstroBox、表盘自定义工具与 Notify 区分说明
上一篇
米坛社区工具下载入口怎么找?AstroBox、表盘自定义工具与 Notify 区分说明
shizuku官方GitHub在哪里?源码仓库、API文档与下载入口关系
下一篇
shizuku官方GitHub在哪里?源码仓库、API文档与下载入口关系
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    258次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    302次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    281次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    259次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    68次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码