性能
双画布的 Canvas2D 渲染管线只重绘发生变化的部分,每一帧的每个步骤的开销都取决于屏幕上显示的内容,
而不是已加载的历史数据量。下面的数据来自 pnpm bench(单核)以及对实际组件的性能分析; 功能实验室会在你点击时为真实的切换计时。
帧耗时:从 500 到 100,000 根K线始终持平
- 双画布——一个场景画布(网格、序列、指标、图表对象、坐标轴),以及一个较薄的顶层画布,用于十字光标、图例和其他跟随指针的元素。鼠标悬停只重绘顶层画布(约 0.2 ms),浏览器合成的是两个图层,而不是四个。
- 只渲染可见范围——每个渲染器、自动缩放以及指标的价格范围计算,都只遍历视野内的K线。
- 每帧不产生垃圾对象——视口快照在两次变化之间会被缓存;数字和日期格式化器会被复用,而不是为每个标签重新创建。
- 自动调整宽度的价格轴——让坐标轴适应最长标签的开销约为 0.05 ms,并且只有在宽度确实变化时才重新布局。
实时 tick:增量计算指标
一次 tick 只会改变正在形成的K线,因此实现了 update() 的指标只需重新计算这一根K线
(SMA、EMA、WMA、VWMA、Bollinger、Envelope、RSI、MACD、ATR、OBV、Stochastic)。
其他指标会回退为完整重算。BB + EMA + RSI + MACD 的数据如下:
| 历史数据 | 完整重算 | 增量 update() |
|---|---|---|
| 20,000 根K线 | 约 5 ms | 约 0.0005 ms |
| 100,000 根K线 | 约 27 ms | 约 0.001 ms |
自定义指标也可以选择支持——参见 插件 → 快速实时更新。
切换品种或周期
- 低开销的完整加载——指标值存放在
IndicatorValueMap中(基于数组,构建开销约为以时间戳为键的Map的三分之一),并且setData会复用仍然有效的K线,而不是复制它们。 - 只在慢的时候显示加载——上一个图表会保留在屏幕上;只有切换超过 200 ms 才会显示遮罩,因此正常约 130 ms 的网络切换永远不会闪烁。
- 不会出现过期数据——已被取代的历史数据请求会被丢弃,旧的 socket 会被断开,因此始终以最后一次点击为准。
- 本地重采样——静态数据切换周期时无需重新请求;较慢的重采样会在主线程忙碌之前先绘制遮罩。
LTTB 降采样
当K线数量远多于像素时,折线图和面积图会使用 Largest-Triangle-Three-Buckets 算法把可见范围降采样到每像素约 2 个点—— 线条在视觉上完全一致,而绘制的点数少了几十倍。在正常缩放级别下不做任何处理。 该算法也已导出,可供你自己使用:
import { lttbDownsample } from '@tradecanvas/chart'
// indices preserving the shape of a 100k series, reduced to 1600 points
const idx = lttbDownsample(series.length, 1600, (i) => series[i].close) | 可见点数 → 1600 | 每帧耗时 | 吞吐量 |
|---|---|---|
| 10,000 | 约 0.025 ms | 39,600 次 / 秒 |
| 100,000 | 约 0.32 ms | 3,100 次 / 秒 |
| 1,000,000 | 约 2.6 ms | 380 次 / 秒 |
缩小时的指标
当每根K线窄于一个像素时,指标的线、带和柱状图按像素列绘制:每列一段,从该列最低点到最高点,与前一列相连,宽度等于线宽。它看起来与穿过数千个点的描边几乎一样,光栅化的工作量却少得多。在 200,000 根K线上缩小并叠加布林带、EMA、RSI 和 MACD 时,集成显卡上每帧从约 54 ms 降到约 21 ms。node scripts/bench-render.mjs 可在你自己的机器上跑出这些数字。
WebGL 渲染器(预览)
renderer: 'webgl' 用 WebGL 2 绘制图表区和指标窗格,画在 2D 场景下方的一个画布上:网格、交易时段、K线和成交量直接绘制;指标、对比线和大多数图表类型的 Canvas 2D 绘制会被记录成 GPU 上的线条、填充和矩形,边缘像 Canvas 2D 一样抗锯齿。文字、绘图、订单、坐标轴和十字线仍用 Canvas 2D,GPU 无法画得一致的内容也交给 Canvas 2D(按原顺序绘制,层叠顺序不变);自定义指标插件无需修改即可使用。K线与 Canvas 2D 的差异不超过 2/255;线条和填充只在少数抗锯齿边缘像素上有差别。WebGL 代码是一个独立的 chunk(gzip 后约 17 KB),首次使用时才加载;没有 WebGL 2 或上下文丢失时,图表会继续用 Canvas 2D 绘制。
const chart = new Chart(el, { renderer: 'webgl' })
chart.on('rendererChange', (e) => console.log(e.payload))
// { renderer: 'webgl' }, or { renderer: 'canvas', reason: 'unsupported' | 'contextLost' }
await chart.setRenderer('canvas') // resolves to what draws now 平移时的每帧耗时,集成显卡(Intel UHD;16.7 ms 即 60 fps):
| 图表 | 像素比 | Canvas 2D | WebGL |
|---|---|---|---|
| 1600×900,500 根K线 + 4 个指标 | 2 | 27.4 ms | 19.6 ms |
| 1600×900,在 200,000 根K线上缩小 + 4 个指标 | 2 | 34.5 ms | 20.2 ms |
| 六个图表,每个 500 根K线加两个指标 | 2 | 23.5 ms | 17.2 ms |
| 2560×1400,2,000 根K线 + 4 个指标 | 1 | 41.6 ms | 17.7 ms |
| 2560×1400,2,000 根K线 + 4 个指标 | 1.5 | 70.8 ms | 17.6 ms |
| 2560×1400,2,000 根K线 + 4 个指标 | 2 | 114.5 ms | 29.1 ms |
| 2560×1400,2,000 根K线 | 2 | 33.1 ms | 20.9 ms |
上表中 WebGL 的大多数帧都在 16.7 ms;平均值包含了少数较长的帧。像素比 2、2560×1400 的图表上,仅浏览器合成全尺寸图层就要在这块 GPU 上花约 23 ms。node scripts/bench-render.mjs --renderer=webgl 可在你的机器上测出这些数字。
脱离主线程
IndicatorWorkerHost 在 Web Worker 中运行指标计算,提供基于 Promise 的 calculate()、按请求设置的超时,以及用于 SSR 和测试的同步回退方案。