行情系统为什么越做越慢?
很多人做行情系统,都会经历一个阶段: 一开始很流畅。 REST 拉数据,页面 setInterval 刷新,数字在跳,一切正常。 后来升级成 WebSocket 实时推送,心里还挺高兴—— “终于实时了。” 但奇怪的事情发生了: 于是第一反应往往是: 但现实是—— 很多时候,服务器很健康。 今天我们不谈后端,不谈分发优化。 为什么前端行情页面会越来越慢? 浏览器的主线程负责: 这些事情,全在一个线程里完成。 而浏览器为了保持流畅,理想状态是: 如果某一段 JS 执行超过 16ms, 掉帧的表现就是: 所以实时行情的真正敌人,不是网络, 假设你的 WebSocket 每秒推送 50 条数据。 浏览器收到消息后会执行: 问题在哪? JSON.parse 本身是同步执行的。 它会: 当数据量一多,真正慢的不是 parse, 每秒 50 次 parse, 浏览器就会频繁触发 GC。 GC 触发时,主线程暂停。 暂停 20ms,用户就能明显感觉卡顿。 1️⃣ 批量处理 不要每条消息立刻更新 UI。 可以先入队: 我们肉眼感知 200ms 内的变化已经足够实时。 推荐的批量处理结构: 示例代码: 这样做的本质是:降低渲染频率,而不是降低实时性。 2️⃣ 降低推送频率 不是每个 tick 都必须渲染。 可以做: 实时 ≠ 每条都渲染。 3️⃣ 使用 Web Worker 把 JSON 解析放到 Worker 中。 主线程只接收处理结果。 这样 decode 不会阻塞 UI。 优化后的数据流模型 示例代码: main-thread.js worker.js 主线程只负责调度,不负责重计算。 很多人写 K 线更新逻辑是这样的: 这在少量数据时没问题。 但当数据增长到: 全量重绘的成本会越来越高。 图表库需要: 这会造成明显卡顿。 K 线本质是时间序列。 时间序列有一个特点: 所以正确方式是: 避免整图刷新。 问题: 时间序列只会向前生长,不应该反复重建。 很多专业图表库(例如 Lightweight Charts) 关键是:你是否用对了方式。 常见写法: 页面运行 1 小时后: 访问复杂度从 O(1) 变成 O(n)。 最终表现为: “刚打开还行,用久了就卡。” 1️⃣ 使用 Ring Buffer 固定长度数组,超出后覆盖旧数据。 示例实现: 数据有上限,系统才稳定。 2️⃣ 时间分桶 不要保存所有 tick, 3️⃣ 限制可见范围 用户屏幕只能看到 100 根, 滑动时再加载历史。 当你升级成 WebSocket 后, 问题往往不是: 而是: WebSocket 只是放大了问题。 因为数据更频繁了。 总结为四句话: 1️⃣ 合并更新,不要逐条渲染 实时系统的核心思想不是“快”。 而是“控制节奏”。 很多人认为: “越实时越好。” 但真实世界是: 所以真正优秀的实时系统, 不是把每条数据都渲染出来, 而是: 行情系统变慢,往往不是接口问题。 也不是服务器问题。 而是客户端架构问题。 当数据频率提高时, 单线程模型会暴露出所有设计缺陷。 如果不改变前端架构, WebSocket 只会让页面更快崩溃。 如果你正在构建实时行情系统,行情系统为什么越做越慢?
——前端性能崩塌的真正原因(客户端深度拆解)
服务器是不是扛不住了?
真正拖垮系统的,是浏览器自己。
只聊一个问题:一、先理解一个基本事实:浏览器是单线程

每 16ms 完成一次渲染(约 60FPS)
这一帧就会掉帧。
而是主线程占用时间。二、JSON 解析为什么会拖垮页面?
message → JSON.parse → 数据处理 → 更新图表
而是 对象创建 + 垃圾回收(GC)。
每次创建几十个对象,
几分钟后内存开始膨胀,
如何优化 JSON 解码?
queue.push(message)
每 200ms 统一处理一次WebSocket
↓
onmessage
↓
queue.push(rawMessage)
↓
(定时器 200ms)
↓
批量取出 queue
↓
合并数据
↓
一次性更新图表const queue = []
let timer = null
ws.onmessage = (event) => {
queue.push(event.data)
}
timer = setInterval(() => {
if (queue.length === 0) return
const batch = queue.splice(0, queue.length)
const parsed = batch.map(msg => JSON.parse(msg))
updateChart(parsed)
}, 200)
实时 = 肉眼可感知实时。tick1 tick2 tick3
↓ ↓ ↓
WebSocket onmessage
↓
postMessage → Web Worker
↓
Worker 中 JSON.parse
↓
解析后的数据 → 主线程
↓
queue.push(parsedData)
↓
每 200ms 批量渲染const worker = new Worker('worker.js')
const queue = []
ws.onmessage = (e) => {
worker.postMessage(e.data)
}
worker.onmessage = (e) => {
queue.push(e.data)
}self.onmessage = (e) => {
const parsed = JSON.parse(e.data)
self.postMessage(parsed)
}三、K 线为什么“越更新越卡”?
每条 tick 到来:
→ setOption()
→ 重绘整张图正确的更新思路是什么?
只会在末尾追加数据。
❌ 错误方式
ws.onmessage = (tick) => {
chart.setOption({
series: [{ data: fullKlineData }]
})
}✅ 正确方式
ws.onmessage = (tick) => {
const last = klineData[klineData.length - 1]
if (samePeriod(tick, last)) {
last.close = tick.price
} else {
klineData.push(newBar(tick))
}
chart.update(last) // 只更新末尾
}
都支持增量更新。四、数据结构错误会让页面慢慢“自杀”
tickdb.push(newTick)更合理的结构
class RingBuffer {
constructor(size) {
this.size = size
this.buffer = new Array(size)
this.index = 0
}
push(item) {
this.buffer[this.index] = item
this.index = (this.index + 1) % this.size
}
toArray() {
return [
...this.buffer.slice(this.index),
...this.buffer.slice(0, this.index)
]
}
}
只保留聚合后的 K 线。
没必要在内存中维护 5000 根。五、真正的性能瓶颈在哪里?
六、行情前端的正确设计思路
2️⃣ 局部更新,不要整图刷新
3️⃣ 限制内存,不要无限增长
4️⃣ 主线程只做必要工作七、一个关键认知升级
在肉眼感知范围内,控制系统稳定。
结语
也可以参考我们整理的 Demo 与接口实现示例:
后续会持续更新性能优化与架构实践内容。