前言

最近在开发快艇骰子项目时,遇到了一个非常折磨人的问题。

房间页面进入后总感觉特别卡,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,而是学会一步一步排除错误的能力。

很多时候,最难发现的问题,往往就藏在自己写的代码里。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐