パフォーマンス
2 枚のキャンバスを使う Canvas2D のパイプラインは変化した部分だけを再描画し、フレームごとの処理はすべて、
読み込んだ履歴の量ではなく画面に表示されている範囲で上限が決まります。以下の数値は pnpm bench(シングルコア)と、実際のウィジェットのプロファイリングによるものです。 Feature Lab では、クリックした切り替えの実際の時間を計測できます。
フレーム:500 本から 100,000 本まで一定
- 2 枚のキャンバス — シーン用のキャンバス(グリッド、系列、インジケーター、チャートオブジェクト、軸)と、クロスヘアや凡例などポインターに連動する表示のための薄い最上位キャンバスです。ホバー時は最上位キャンバスだけを再描画し(約 0.2 ms)、ブラウザーが合成するのも 4 枚ではなく 2 枚で済みます。
- 表示範囲だけの描画 — すべてのレンダラー、自動スケール、インジケーターの価格範囲の計算は、表示中のバーだけを走査します。
- フレームごとのガベージなし — ビューポートのスナップショットは変更があるまでキャッシュされ、数値と日付のフォーマッターはラベルごとに作り直さず再利用されます。
- 価格軸の自動サイズ調整 — 軸の幅を最も長いラベルに合わせるコストは約 0.05 ms で、幅が実際に変わったときだけ再レイアウトします。
ライブのティック:インジケーターの差分計算
ティックで動くのは形成中のバーだけなので、update() を実装したインジケーター
(SMA、EMA、WMA、VWMA、Bollinger、Envelope、RSI、MACD、ATR、OBV、Stochastic)はそのバーだけを再計算します。
それ以外は全体の再計算にフォールバックします。BB + EMA + RSI + MACD の場合:
| 履歴 | 全体の再計算 | 差分計算の update() |
|---|---|---|
| 20,000 本 | 約 5 ms | 約 0.0005 ms |
| 100,000 本 | 約 27 ms | 約 0.001 ms |
カスタムインジケーターも対応できます。プラグイン → 高速なライブ更新を参照してください。
シンボルや時間足の切り替え
- 軽いフルロード — インジケーターの値は
IndicatorValueMap(配列ベースで、タイムスタンプをキーとするMapより約 3 倍低コストで構築できます)に保持され、setDataはすでに有効なバーをコピーせずに再利用します。 - 遅いときだけ読み込み表示 — 前のチャートは表示されたままで、切り替えが 200 ms を超えたときだけ読み込み中の覆いが現れます。そのため、通常の約 130 ms のネットワーク切り替えで画面がちらつくことはありません。
- 古いデータを表示しない — 置き換えられた履歴リクエストは破棄され、古いソケットは切り離されるため、常に最後のクリックが反映されます。
- ローカルでのリサンプリング — 静的なデータは再取得なしで時間足を切り替えます。時間のかかるリサンプリングでは、メインスレッドが処理でふさがる前に読み込み中の覆いを描きます。
LTTB ダウンサンプリング
ピクセル数よりはるかに多くのバーがある場合、ラインチャートとエリアチャートは表示範囲を Largest-Triangle-Three-Buckets で 1 ピクセルあたり約 2 点にダウンサンプリングします。 描く点の数は数十分の 1 になりますが、ラインの見た目は変わりません。通常のズームでは何もしません。 このアルゴリズムはエクスポートされているので、独自に使うこともできます:
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 / 秒 |
縮小時のインジケーター
1 本のバーが 1 ピクセルより細くなると、インジケーターのライン、バンド、ヒストグラムはピクセル列ごとに 1 本のスパンで描かれます。列の最安値から最高値まで、前の列とつながり、幅は線の太さと同じです。数千点を通るストロークとほぼ同じ見た目のまま、ラスタライズの手間はずっと少なくなります。200,000 本のバーを縮小表示し、ボリンジャーバンド、EMA、RSI、MACD を重ねた場合、内蔵 GPU で 1 フレームが約 54 ms から約 21 ms になりました。node scripts/bench-render.mjs で手元のマシンでも計測できます。
WebGL レンダラー(プレビュー)
renderer: 'webgl' を指定すると、プロット領域とインジケーターのペインを WebGL 2 で、2D シーンの下にあるキャンバスに描きます。グリッド、セッション、ローソク足、出来高は直接描き、インジケーター、比較ライン、ほとんどのチャートタイプの Canvas 2D 描画は GPU の線・塗り・矩形として記録し、Canvas 2D と同じように縁をアンチエイリアスします。テキスト、描画、注文、軸、クロスヘアは Canvas 2D のままで、GPU で同じに描けないものも Canvas 2D に任せます(順番どおりに描くので重なり順は変わりません)。自作のインジケータープラグインも変更なしで動きます。ローソク足は Canvas 2D と 2/255 以内で一致し、線と塗りはアンチエイリアスされた縁のわずかなピクセルだけが異なります。WebGL のコードは独立したチャンク(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 パン中の 1 フレームの時間、内蔵 GPU(Intel UHD、16.7 ms で 60 fps):
| チャート | ピクセル比 | Canvas 2D | WebGL |
|---|---|---|---|
| 1600×900、ローソク足 500 本 + インジケーター 4 つ | 2 | 27.4 ms | 19.6 ms |
| 1600×900、200,000 本を縮小表示 + インジケーター 4 つ | 2 | 34.5 ms | 20.2 ms |
| チャート 6 つ、それぞれローソク足 500 本とインジケーター 2 つ | 2 | 23.5 ms | 17.2 ms |
| 2560×1400、ローソク足 2,000 本 + インジケーター 4 つ | 1 | 41.6 ms | 17.7 ms |
| 2560×1400、ローソク足 2,000 本 + インジケーター 4 つ | 1.5 | 70.8 ms | 17.6 ms |
| 2560×1400、ローソク足 2,000 本 + インジケーター 4 つ | 2 | 114.5 ms | 29.1 ms |
| 2560×1400、ローソク足 2,000 本 | 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 は、Promise ベースの calculate()、リクエストごとのタイムアウト、
SSR やテスト向けの同期フォールバックを備え、インジケーターの計算を Web Worker で実行します。