SharedWorker 必须独立实现心跳机制,使用 setInterval 在 Worker 内定时发送轻量 ping 消息(如 {type:"ping",ts:...}),避开页面后台冻结影响;服务端需响应 pong,Worker 连续两次未收到则触发状态机驱动的指数退避重连;页面仅监听状态广播,不参与心跳逻辑。
SharedWorker 必须自己发心跳,页面切后台后主线程定时器会停
页面主线程的
在标签页被切换到后台或进入冻结状态(如 Chrome 的 `throttled` 状态)后,会大幅降频甚至暂停执行。这意味着你放在 React 组件或普通 JS 里的心跳逻辑,大概率在用户切走后就失效了,WebSocket 连接会在几十秒内被服务端或中间代理(如 Nginx、CDN)主动断开。
SharedWorker 不受页面活跃状态影响——只要还有同源页面打开,它就持续运行。所以心跳必须下沉到 SharedWorker 内部,用
或
循环触发,且不能依赖任何页面传来的状态或时间戳。
心跳间隔建议设为
ms,避开浏览器常见的 30s 冻结阈值
不要用
做唯一标识,改用自增序号或
(需检查兼容性)
服务端必须响应
或带相同
的确认包,否则 Worker 应视为超时
连续两次未收到响应,就调用
并触发重连,避免假连接占用资源
心跳消息体要轻,别塞 JSON 或大 payload
SharedWorker 和页面之间靠
通信,频繁发送大消息会拖慢主线程解析、增加内存拷贝开销,尤其在低端设备上容易引发卡顿或 GC 暂停。心跳不是数据通道,它的唯一作用是保活和探测连通性。
推荐格式:
(
用毫秒时间戳已足够)
绝对避免在心跳里附带用户数据、token、session 信息等无关字段
服务端回包也应极简:
,不加额外嵌套
实测表明,消息体超过
时,在 10+ 标签页并发下,Worker 的
调用延迟明显上升
心跳失败 ≠ 立即重连,得配合连接状态机判断
只靠“没收到 pong”就重连,容易误判。比如网络抖动导致某次 pong 延迟到达,但 Worker 已关闭旧连接并新建一个,结果造成双连、重复订阅、消息乱序等问题。必须把心跳结果纳入完整的连接状态机中统一决策。
定义明确状态:
、
、
、
仅当
且连续两次心跳无响应时,才执行
+ 指数退避重连
若当前状态是
或
,忽略心跳超时,防止干扰建连流程
每次重连前检查
实例是否还存在、是否为
状态,避免重复
页面不参与心跳逻辑,只接收状态广播
页面不该知道心跳怎么发、多久一次、失败后怎么做。它的职责只有两件事:初始化 SharedWorker、监听连接状态变化。所有心跳细节必须封装在 Worker 内,对外只暴露干净的状态事件。
Worker 应定期通过
向所有 port 广播当前连接态
页面根据
字段控制 UI(如连接指示器、重试按钮),而不是自己去轮询或计时
禁止页面发送
类指令给 Worker——这等于把心跳控制权又交回主线程,违背设计初衷
如果页面需要“手动触发心跳测试”,应发
,Worker 单独处理,不干扰主心跳流
真正难的不是写几行
,而是让心跳行为完全脱离页面生命周期、不因任意一个 tab 的冻结/崩溃而中断,同时又不干扰正常的消息分发与错误恢复路径。多数人栽在状态判断太粗放,或者把页面当成了 Worker 的协作者而非纯消费者。
setIntervalsetIntervalsetTimeout25000–45000Date.now()self.crypto.randomUUID()"pong"idws.close()port.postMessage(){ type: "ping", ts: 1746001234567 }ts{ type: "pong", ts: 1746001234567 }1KBpostMessage"connecting""open""reconnecting""closed"ws.readyState === WebSocket.OPENws.close()"connecting""reconnecting"wsCLOSEDnew WebSocket()port.postMessage({ type: "status", state: "open" })state{ type: "heartbeat" }{ type: "debug-ping" }setInterval