sharedworker 无法使用 fetch/xhr 发起心跳,必须通过 websocket 发送 ping/pong 帧实现链路保活;需设 25–45 秒间隔、连续两次无 pong 才断连;心跳 id 需唯一且绑定 port 生命周期,并与业务消息严格分离。

SharedWorker 本身不能直接发起网络心跳(比如 fetch 或 XMLHttpRequest),所谓“网络心跳管道”实际是指它托管的 WebSocket 连接所承载的心跳帧——真正的网络层保活必须由 WebSocket 实例完成,SharedWorker 只负责统一调度、发送和响应逻辑。
SharedWorker 中无法用 fetch/XHR 发起心跳,必须依赖 WebSocket
SharedWorker 全局作用域不支持 fetch、XMLHttpRequest、EventSource 等网络 API,浏览器会直接抛 ReferenceError 或静默失败。唯一可用的实时双向通道是 WebSocket,因此“心跳”只能是 ws.send("ping") 这类协议级消息,而非 HTTP 请求。
- 试图在 SharedWorker 里调用
fetch("/health")会报错:ReferenceError: fetch is not defined -
WebSocket是 SharedWorker 唯一能主动建立并维持的长连接机制,所有心跳必须走它 - 服务端必须配合实现
"ping"/"pong"协议解析,不能只靠 TCP keepalive
心跳频率与超时判定必须避开浏览器后台冻结阈值
Chrome/Firefox 对非活跃 tab 的定时器(setInterval)会节流甚至暂停,若心跳间隔设为 60s,页面切后台后可能连续数分钟无响应,导致误判断连。SharedWorker 虽独立于页面,但其定时器仍受同源 tab 活跃状态间接影响(尤其旧版 Safari)。
- 推荐心跳间隔:25–45 秒(避开 60s 冻结线,也留出服务端处理余量)
- 超时判定不能只看单次未响应,应连续 2 次无
"pong"才触发断连,避免瞬时抖动误伤 - 不要在
onmessage回调里直接发心跳——它只响应页面消息,不是连接保活入口
心跳消息必须携带唯一请求 ID 并绑定 port 生命周期
SharedWorker 向服务端发 "ping" 是全局行为,但服务端回的 "pong" 需被正确路由回对应页面 UI(比如更新连接指示器)。如果所有页面共用一个心跳 ID,就无法区分谁该刷新状态;若不绑定 port,页面关闭后心跳响应仍可能投递到已失效端口,引发 port.postMessage is not a function 错误。
- 每次发 ping 时生成唯一
id: crypto.randomUUID(),并缓存Map<id portid></id> - 服务端 pong 消息需原样返回该
id,Worker 查表后只向对应port推送{ type: "heartbeat", ok: true, id } - 页面卸载前调用
worker.port.close(),Worker 在port.onclose中清理对应缓存,避免 ID 泄漏
服务端必须区分“连接心跳”和“业务心跳”,否则推送压力不降反升
很多团队把业务消息(如用户在线状态变更)也塞进心跳通道,结果服务端每秒要广播数百条“我还在”的消息给所有客户端,反而放大了带宽和 CPU 压力。真正的降压关键在于:让服务端只对真实数据变更做推送,心跳仅用于链路存活探测,两者协议分离、通道隔离。
- WebSocket 消息体必须有
type字段:"ping"/"pong"/"data"/"error" - 服务端收到
"ping"后,**只回"pong",不做任何业务广播或 DB 查询** - 页面侧 UI 更新(如绿色小圆点)应基于 Worker 发来的
{ type: "status", state: "connected" },而非等待某条业务消息到达
最容易被忽略的一点是:SharedWorker 的心跳逻辑一旦写死在脚本里,就无法动态调整间隔或开关——它不像页面 JS 可以监听配置中心变更。真要支持灰度或降级,得让页面首次连接时通过 { type: "init", heartbeatInterval: 30000 } 把参数传进来,Worker 存入全局变量,并在 setInterval 外层加个开关标志位。否则上线后发现心跳太密拖垮服务端,只能发版修复。











