瞌睡不醒的工程笔记
关注实时行情系统与数据 API 的工程实践与设计边界。

导航

 

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

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

很多人做行情系统,都会经历一个阶段:

一开始很流畅。

REST 拉数据,页面 setInterval 刷新,数字在跳,一切正常。

后来升级成 WebSocket 实时推送,心里还挺高兴——

“终于实时了。”

但奇怪的事情发生了:

  • 页面开始卡顿
  • 鼠标拖动不顺畅
  • K 线有明显延迟
  • CPU 占用飙升

于是第一反应往往是:

服务器是不是扛不住了?

但现实是——

很多时候,服务器很健康。
真正拖垮系统的,是浏览器自己。

今天我们不谈后端,不谈分发优化。
只聊一个问题:

为什么前端行情页面会越来越慢?


一、先理解一个基本事实:浏览器是单线程

浏览器的主线程负责:

  • 网络回调
  • JSON 解析
  • 数据计算
  • 图表绘制
  • 用户交互
  • DOM 更新

这些事情,全在一个线程里完成。

article4-p1.png

而浏览器为了保持流畅,理想状态是:

每 16ms 完成一次渲染(约 60FPS)

如果某一段 JS 执行超过 16ms,
这一帧就会掉帧。

掉帧的表现就是:

  • 卡顿
  • 拖动不流畅
  • 鼠标延迟

所以实时行情的真正敌人,不是网络,
而是主线程占用时间。


二、JSON 解析为什么会拖垮页面?

假设你的 WebSocket 每秒推送 50 条数据。

浏览器收到消息后会执行:

message → JSON.parse → 数据处理 → 更新图表

问题在哪?

JSON.parse 本身是同步执行的。

它会:

  • 解析字符串
  • 创建大量对象
  • 分配内存

当数据量一多,真正慢的不是 parse,
而是 对象创建 + 垃圾回收(GC)

每秒 50 次 parse,
每次创建几十个对象,
几分钟后内存开始膨胀,

浏览器就会频繁触发 GC。

GC 触发时,主线程暂停。

暂停 20ms,用户就能明显感觉卡顿。

article4-p2.png

如何优化 JSON 解码?

1️⃣ 批量处理

不要每条消息立刻更新 UI。

可以先入队:

queue.push(message)
每 200ms 统一处理一次

我们肉眼感知 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)

这样做的本质是:降低渲染频率,而不是降低实时性。


2️⃣ 降低推送频率

不是每个 tick 都必须渲染。

可以做:

  • 节流
  • 合并
  • 只保留最后一条

实时 ≠ 每条都渲染。
实时 = 肉眼可感知实时。


3️⃣ 使用 Web Worker

把 JSON 解析放到 Worker 中。

主线程只接收处理结果。

这样 decode 不会阻塞 UI。

优化后的数据流模型

tick1  tick2  tick3
   ↓      ↓      ↓
WebSocket onmessage
   ↓
postMessage → Web Worker
   ↓
Worker 中 JSON.parse
   ↓
解析后的数据 → 主线程
   ↓
queue.push(parsedData)
   ↓
每 200ms 批量渲染

示例代码:

main-thread.js

const worker = new Worker('worker.js')
const queue = []

ws.onmessage = (e) => {
  worker.postMessage(e.data)
}

worker.onmessage = (e) => {
  queue.push(e.data)
}

worker.js

self.onmessage = (e) => {
  const parsed = JSON.parse(e.data)
  self.postMessage(parsed)
}

主线程只负责调度,不负责重计算。


三、K 线为什么“越更新越卡”?

很多人写 K 线更新逻辑是这样的:

每条 tick 到来:
→ setOption()
→ 重绘整张图

这在少量数据时没问题。

但当数据增长到:

  • 500 根 K 线
  • 1000 根 K 线
  • 多时间周期切换

全量重绘的成本会越来越高。

图表库需要:

  • diff 数据
  • 重新布局
  • 重新绘制 canvas
  • 分配新数组

这会造成明显卡顿。


正确的更新思路是什么?

K 线本质是时间序列。

时间序列有一个特点:

只会在末尾追加数据。

所以正确方式是:

  • 如果是当前周期 → 更新最后一根
  • 如果进入新周期 → append 一根

避免整图刷新。

❌ 错误方式

ws.onmessage = (tick) => {
  chart.setOption({
    series: [{ data: fullKlineData }]
  })
}

问题:

  • 每次重建数组
  • 每次全量 diff
  • 每次触发重绘

✅ 正确方式

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) // 只更新末尾
}

时间序列只会向前生长,不应该反复重建。

很多专业图表库(例如 Lightweight Charts)
都支持增量更新。

关键是:你是否用对了方式。


四、数据结构错误会让页面慢慢“自杀”

常见写法:

tickdb.push(newTick)

页面运行 1 小时后:

  • 数组几万条
  • 内存持续增长
  • GC 越来越频繁

访问复杂度从 O(1) 变成 O(n)。

最终表现为:

“刚打开还行,用久了就卡。”


更合理的结构

1️⃣ 使用 Ring Buffer

固定长度数组,超出后覆盖旧数据。

示例实现:

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)
    ]
  }
}

数据有上限,系统才稳定。

2️⃣ 时间分桶

不要保存所有 tick,
只保留聚合后的 K 线。

3️⃣ 限制可见范围

用户屏幕只能看到 100 根,
没必要在内存中维护 5000 根。

滑动时再加载历史。


五、真正的性能瓶颈在哪里?

当你升级成 WebSocket 后,

问题往往不是:

  • 网络慢
  • 服务器慢

而是:

  • 主线程被 JSON decode 占满
  • 图表频繁重绘
  • 数据结构无上限增长
  • GC 频繁触发

WebSocket 只是放大了问题。

因为数据更频繁了。


六、行情前端的正确设计思路

总结为四句话:

1️⃣ 合并更新,不要逐条渲染
2️⃣ 局部更新,不要整图刷新
3️⃣ 限制内存,不要无限增长
4️⃣ 主线程只做必要工作

实时系统的核心思想不是“快”。

而是“控制节奏”。


七、一个关键认知升级

很多人认为:

“越实时越好。”

但真实世界是:

  • 我们肉眼对 100ms 以内的延迟几乎无感
  • 200ms 内都算流畅
  • 超过 300ms 才会明显感觉延迟

所以真正优秀的实时系统,

不是把每条数据都渲染出来,

而是:

在肉眼感知范围内,控制系统稳定。


结语

行情系统变慢,往往不是接口问题。

也不是服务器问题。

而是客户端架构问题。

当数据频率提高时,

单线程模型会暴露出所有设计缺陷。

如果不改变前端架构,

WebSocket 只会让页面更快崩溃。

如果你正在构建实时行情系统,
也可以参考我们整理的 Demo 与接口实现示例
后续会持续更新性能优化与架构实践内容。

posted on 2026-02-26 17:41  瞌睡不醒  阅读(7)  评论(0)    收藏  举报