找回密码
 立即注册
首页 业界区 安全 行情系统为什么越做越慢?

行情系统为什么越做越慢?

厂潺 4 小时前
行情系统为什么越做越慢?

——前端性能崩塌的真正原因(客户端深度拆解)

很多人做行情系统,都会经历一个阶段:
一开始很流畅。
REST 拉数据,页面 setInterval 刷新,数字在跳,一切正常。
后来升级成 WebSocket 实时推送,心里还挺高兴——
“终于实时了。”
但奇怪的事情发生了:

  • 页面开始卡顿
  • 鼠标拖动不顺畅
  • K 线有明显延迟
  • CPU 占用飙升
于是第一反应往往是:
服务器是不是扛不住了?
但现实是——
很多时候,服务器很健康。
真正拖垮系统的,是浏览器自己。
今天我们不谈后端,不谈分发优化。
只聊一个问题:
为什么前端行情页面会越来越慢?
一、先理解一个基本事实:浏览器是单线程

浏览器的主线程负责:

  • 网络回调
  • JSON 解析
  • 数据计算
  • 图表绘制
  • 用户交互
  • DOM 更新
这些事情,全在一个线程里完成。
1.png

而浏览器为了保持流畅,理想状态是:
每 16ms 完成一次渲染(约 60FPS)
如果某一段 JS 执行超过 16ms,
这一帧就会掉帧。
掉帧的表现就是:

  • 卡顿
  • 拖动不流畅
  • 鼠标延迟
所以实时行情的真正敌人,不是网络,
而是主线程占用时间。
二、JSON 解析为什么会拖垮页面?

假设你的 WebSocket 每秒推送 50 条数据。
浏览器收到消息后会执行:
  1. message → JSON.parse → 数据处理 → 更新图表
复制代码
问题在哪?
JSON.parse 本身是同步执行的。
它会:

  • 解析字符串
  • 创建大量对象
  • 分配内存
当数据量一多,真正慢的不是 parse,
而是 对象创建 + 垃圾回收(GC)
每秒 50 次 parse,
每次创建几十个对象,
几分钟后内存开始膨胀,
浏览器就会频繁触发 GC。
GC 触发时,主线程暂停。
暂停 20ms,用户就能明显感觉卡顿。
2.png


如何优化 JSON 解码?

1️⃣ 批量处理
不要每条消息立刻更新 UI。
可以先入队:
  1. queue.push(message)
  2. 每 200ms 统一处理一次
复制代码
我们肉眼感知 200ms 内的变化已经足够实时。
推荐的批量处理结构:
  1. WebSocket
  2.    ↓
  3. onmessage
  4.    ↓
  5. queue.push(rawMessage)
  6.    ↓
  7. (定时器 200ms)
  8.    ↓
  9. 批量取出 queue
  10.    ↓
  11. 合并数据
  12.    ↓
  13. 一次性更新图表
复制代码
示例代码:
  1. const queue = []
  2. let timer = null
  3. ws.onmessage = (event) => {
  4.   queue.push(event.data)
  5. }
  6. timer = setInterval(() => {
  7.   if (queue.length === 0) return
  8.   const batch = queue.splice(0, queue.length)
  9.   const parsed = batch.map(msg => JSON.parse(msg))
  10.   updateChart(parsed)
  11. }, 200)
复制代码
这样做的本质是:降低渲染频率,而不是降低实时性。
2️⃣ 降低推送频率
不是每个 tick 都必须渲染。
可以做:

  • 节流
  • 合并
  • 只保留最后一条
实时 ≠ 每条都渲染。
实时 = 肉眼可感知实时。
3️⃣ 使用 Web Worker
把 JSON 解析放到 Worker 中。
主线程只接收处理结果。
这样 decode 不会阻塞 UI。
优化后的数据流模型
  1. tick1  tick2  tick3
  2.    ↓      ↓      ↓
  3. WebSocket onmessage
  4.    ↓
  5. postMessage → Web Worker
  6.    ↓
  7. Worker 中 JSON.parse
  8.    ↓
  9. 解析后的数据 → 主线程
  10.    ↓
  11. queue.push(parsedData)
  12.    ↓
  13. 每 200ms 批量渲染
复制代码
示例代码:
main-thread.js
  1. const worker = new Worker('worker.js')
  2. const queue = []
  3. ws.onmessage = (e) => {
  4.   worker.postMessage(e.data)
  5. }
  6. worker.onmessage = (e) => {
  7.   queue.push(e.data)
  8. }
复制代码
worker.js
  1. self.onmessage = (e) => {
  2.   const parsed = JSON.parse(e.data)
  3.   self.postMessage(parsed)
  4. }
复制代码
主线程只负责调度,不负责重计算。
三、K 线为什么“越更新越卡”?

很多人写 K 线更新逻辑是这样的:
  1. 每条 tick 到来:
  2. → setOption()
  3. → 重绘整张图
复制代码
这在少量数据时没问题。
但当数据增长到:

  • 500 根 K 线
  • 1000 根 K 线
  • 多时间周期切换
全量重绘的成本会越来越高。
图表库需要:

  • diff 数据
  • 重新布局
  • 重新绘制 canvas
  • 分配新数组
这会造成明显卡顿。
正确的更新思路是什么?

K 线本质是时间序列。
时间序列有一个特点:
只会在末尾追加数据。
所以正确方式是:

  • 如果是当前周期 → 更新最后一根
  • 如果进入新周期 → append 一根
避免整图刷新。
❌ 错误方式
  1. ws.onmessage = (tick) => {
  2.   chart.setOption({
  3.     series: [{ data: fullKlineData }]
  4.   })
  5. }
复制代码
问题:

  • 每次重建数组
  • 每次全量 diff
  • 每次触发重绘
✅ 正确方式
  1. ws.onmessage = (tick) => {
  2.   const last = klineData[klineData.length - 1]
  3.   if (samePeriod(tick, last)) {
  4.     last.close = tick.price
  5.   } else {
  6.     klineData.push(newBar(tick))
  7.   }
  8.   chart.update(last) // 只更新末尾
  9. }
复制代码
时间序列只会向前生长,不应该反复重建。
很多专业图表库(例如 Lightweight Charts)
都支持增量更新。
关键是:你是否用对了方式。
四、数据结构错误会让页面慢慢“自杀”

常见写法:
  1. tickdb.push(newTick)
复制代码
页面运行 1 小时后:

  • 数组几万条
  • 内存持续增长
  • GC 越来越频繁
访问复杂度从 O(1) 变成 O(n)。
最终表现为:
“刚打开还行,用久了就卡。”
更合理的结构

1️⃣ 使用 Ring Buffer
固定长度数组,超出后覆盖旧数据。
示例实现:
  1. class RingBuffer {
  2.   constructor(size) {
  3.     this.size = size
  4.     this.buffer = new Array(size)
  5.     this.index = 0
  6.   }
  7.   push(item) {
  8.     this.buffer[this.index] = item
  9.     this.index = (this.index + 1) % this.size
  10.   }
  11.   toArray() {
  12.     return [
  13.       ...this.buffer.slice(this.index),
  14.       ...this.buffer.slice(0, this.index)
  15.     ]
  16.   }
  17. }
复制代码
数据有上限,系统才稳定。
2️⃣ 时间分桶
不要保存所有 tick,
只保留聚合后的 K 线。
3️⃣ 限制可见范围
用户屏幕只能看到 100 根,
没必要在内存中维护 5000 根。
滑动时再加载历史。
五、真正的性能瓶颈在哪里?

当你升级成 WebSocket 后,
问题往往不是:

  • 网络慢
  • 服务器慢
而是:

  • 主线程被 JSON decode 占满
  • 图表频繁重绘
  • 数据结构无上限增长
  • GC 频繁触发
WebSocket 只是放大了问题。
因为数据更频繁了。
六、行情前端的正确设计思路

总结为四句话:
1️⃣ 合并更新,不要逐条渲染
2️⃣ 局部更新,不要整图刷新
3️⃣ 限制内存,不要无限增长
4️⃣ 主线程只做必要工作
实时系统的核心思想不是“快”。
而是“控制节奏”。
七、一个关键认知升级

很多人认为:
“越实时越好。”
但真实世界是:

  • 我们肉眼对 100ms 以内的延迟几乎无感
  • 200ms 内都算流畅
  • 超过 300ms 才会明显感觉延迟
所以真正优秀的实时系统,
不是把每条数据都渲染出来,
而是:
在肉眼感知范围内,控制系统稳定。
结语

行情系统变慢,往往不是接口问题。
也不是服务器问题。
而是客户端架构问题。
当数据频率提高时,
单线程模型会暴露出所有设计缺陷。
如果不改变前端架构,
WebSocket 只会让页面更快崩溃。
如果你正在构建实时行情系统,
也可以参考我们整理的 Demo 与接口实现示例:
后续会持续更新性能优化与架构实践内容。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册