我以为是网络问题,结果是 React Effect 导致的 WebSocket 反复重连
最近在开发快艇骰子项目时,遇到了一个非常折磨人的问题。房间页面进入后总感觉特别卡,WebSocket 连接时好时坏,有时候玩家列表刚刷新出来又突然消失,控制台里也不断出现连接建立和断开的日志。第一反应当然是:是不是后端问题?于是我开始了长达几个小时的排查。最后发现,问题根本不在网络,也不在后端,而是在前端一个看起来完全正常的 React Effect 上。这篇文章记录一下整个排查过程,希望能帮到以
前言
最近在开发快艇骰子项目时,遇到了一个非常折磨人的问题。
房间页面进入后总感觉特别卡,WebSocket 连接时好时坏,有时候玩家列表刚刷新出来又突然消失,控制台里也不断出现连接建立和断开的日志。
第一反应当然是:
是不是后端问题?
于是我开始了长达几个小时的排查。
最后发现,问题根本不在网络,也不在后端,而是在前端一个看起来完全正常的 React Effect 上。
这篇文章记录一下整个排查过程,希望能帮到以后遇到类似问题的人。
问题现象
项目使用 WebSocket 实现房间实时通信。
正常情况下:
- 用户进入房间
- 建立 WebSocket 连接
- 接收房间消息
- 更新玩家列表
但实际运行时却出现了几个异常现象:
1. 房间连接很慢
进入房间后需要等待几秒钟才能看到完整信息。
有时候甚至需要刷新页面才能正常进入。
2. WebSocket 频繁断开重连
控制台不断打印:
WebSocket Connected
WebSocket Closed
WebSocket Connected
WebSocket Closed
WebSocket Connected
WebSocket Closed
看起来像是在疯狂重连。
3. 玩家列表不断刷新
房间里玩家明明没有变化。
但页面却一直重新渲染。
甚至出现玩家列表闪烁的情况。
我最开始怀疑什么
遇到实时通信问题的时候,我的第一反应其实和很多人一样。
怀疑后端
因为 WebSocket 是后端提供的服务。
连接不稳定?
那大概率是服务有问题吧。
于是我开始询问后端同学:
- 服务有没有重启?
- 房间频道是不是挂了?
- 有没有限流?
结果后端日志一切正常。
怀疑网络
后来又怀疑是不是网络问题。
测试了:
- 手机热点
- 公司网络
结果问题依旧存在。
怀疑 WebSocket 服务
甚至开始怀疑:
是不是 WebSocket 本身有问题?
是不是连接超时了?
结果检查下来也没有发现异常。
开始真正排查
当排除了网络和后端问题后。
我决定回到前端代码本身。
查看 Network
打开浏览器开发者工具。
发现一个奇怪现象:
每隔一段时间都会出现新的 WebSocket 请求。
正常情况下:
建立一次连接
↓
持续通信
实际情况:
建立连接
↓
关闭
↓
重新建立
↓
关闭
↓
重新建立
这说明不是服务器主动断开。
而是前端在不断重新创建连接。
查看 Console
给 WebSocket 生命周期打日志:
console.log('connect')
return () => {
console.log('disconnect')
}
结果控制台疯狂输出:
connect
disconnect
connect
disconnect
connect
disconnect
看到这里已经基本确定:
问题发生在 React 组件内部。
最终定位
继续追踪代码。
发现房间连接逻辑大概是这样:
const enterMatch = useCallback(() => {
...
}, [players])
useEffect(() => {
const cleanup = connectRoomChannel()
return cleanup
}, [enterMatch])
看起来似乎没什么问题。
但仔细分析依赖关系:
players变化
↓
enterMatch重新创建
↓
effect依赖变化
↓
执行cleanup
↓
关闭旧连接
↓
重新建立连接
整个链路如下:
players变化
↓
enterMatch变化
↓
effect重新执行
↓
cleanup
↓
重新建立WebSocket
而房间消息本身又会更新 players。
于是形成了死循环。
收到消息
↓
更新players
↓
effect重新执行
↓
重建连接
↓
收到消息
↓
更新players
↓
再次重建连接
这就是问题的根源。
根本原因
很多时候我们会觉得:
useEffect(() => {}, [xxx])
只要依赖写全就没问题。
实际上并不是。
对于 WebSocket 这种长连接来说:
Effect 的依赖必须非常稳定。
因为每次依赖变化都会触发:
cleanup()
重新执行effect()
如果依赖的是频繁变化的数据:
players
messages
roomInfo
那么连接就会不断重建。
最终导致:
- 重连频繁
- 页面卡顿
- 消息异常
- 状态错乱
如何修复
方案一:依赖隔离
不要让 WebSocket Effect 依赖频繁变化的数据。
例如:
useEffect(() => {
const cleanup = connectRoomChannel()
return cleanup
}, [roomId])
只在房间变化时建立连接。
方案二:使用 useRef 保存实时数据
如果连接内部需要最新数据。
不要把数据放到 Effect 依赖里。
而是使用 Ref。
const playersRef = useRef(players)
useEffect(() => {
playersRef.current = players
}, [players])
这样:
- 连接不重建
- 数据仍然保持最新
方案三:谨慎使用 useCallback
很多时候:
useCallback(...)
并没有真正减少变化。
反而因为依赖项频繁变化。
导致函数不断重新创建。
所以一定要检查:
useCallback
↓
依赖谁
↓
依赖是否稳定
这次踩坑最大的收获
以前遇到实时通信问题。
我的排查顺序是:
网络
↓
后端
↓
WebSocket服务
↓
前端
而这次之后变成了:
前端Effect依赖
↓
状态更新逻辑
↓
WebSocket生命周期
↓
后端
因为很多看起来像网络问题的问题。
实际上都是前端自己造成的。
总结
这次问题最终不是网络问题。
不是服务器问题。
甚至不是 WebSocket 的问题。
而是一个 React Effect 依赖设计不合理导致的连锁反应。
问题链路其实非常简单:
players变化
↓
enterMatch变化
↓
effect重新执行
↓
cleanup
↓
重建WebSocket
↓
再次收到消息
↓
players变化
但如果没有耐心一点点排查,很容易把锅甩给网络或者后端。
这次经历让我真正意识到:
调试能力的提升,不是学会更多 API,而是学会一步一步排除错误的能力。
很多时候,最难发现的问题,往往就藏在自己写的代码里。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)