SharedWorker 必须独立实现心跳机制,使用 setInterval 在 Worker 内定时发送轻量 ping 消息(如 {type:"ping",ts:...}),避开页面后台冻结影响;服务端需响应 pong,Worker 连续两次未收到则触发状态机驱动的指数退避重连;页面仅监听状态广播,不参与心跳逻辑。

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

















