实时协作类应用最难的不是逻辑,是带宽。当上行只有 3Mbps、单帧状态又有几十个对象时,JSON 会把你压死——不是性能,是流量。
第一步:把 JSON 去掉
每帧的字段名完全可以省略。约定一个固定顺序,用数组代替对象:
// 之前:每帧 240 字节
{ "players": [{ "id": "a1", "x": 12, "y": 33, "len": 7 }] }
// 之后:每帧 14 字节
[1, 12, 33, 7]
字段名在协议里只出现一次,之后都是位置约定。这一项直接砍掉 90% 的体积。
第二步:只发变化
全量推送没有必要。客户端保留上一帧,服务端只发 diff:
def diff(prev, cur):
out = []
for pid, obj in cur.items():
if prev.get(pid) != obj:
out.append(pack(pid, obj))
return out
安静的时候,一帧就是几个字节。热闹的时候才会涨,但热闹的时候用户注意力也在别处,掉几帧没人会发现。
第三步:二进制 + base64
SSE 是文本协议,二进制要先转 base64。看起来会膨胀 33%,但因为前面已经砍掉了 90%,最终结果仍然远小于原始 JSON。
| 方案 | 单帧 | 10 人 20fps |
|---|---|---|
| 原始 JSON | 2.4 KB | 480 KB/s |
| 数组化 JSON | 210 B | 42 KB/s |
| 二进制 + base64 | 96 B | 19 KB/s |
只要协议是你自己定的,压缩永远有空间。真正的成本不在带宽,在于你愿不愿意写一个自定义的编解码器。
客户端插值
服务端 20fps,屏幕 60fps,中间的空档靠客户端补。简单线性插值加一点缓动,肉眼完全看不出差别,但服务端压力降到三分之一。
const t = (now - lastFrameAt) / FRAME_MS;
render(lerp(prevState, nextState, Math.min(t, 1)));
整个方案跑在两核机器上,CPU 常年在 8% 以下。省下来的不是钱,是那种"够用"的踏实感。