Esta página ainda não foi traduzida, por isso aparece em inglês.

Performance

A two-canvas Canvas2D pipeline repaints only what changed, and every per-frame step is bounded by what is on screen, not by how much history is loaded. The numbers below come from pnpm bench (single core) and from profiling the live widget; the Feature Lab times real switches as you click.

Frames: flat from 500 to 100,000 bars

  • Two canvases — a scene canvas (grid, series, indicators, chart objects, axes) and a thin top canvas for the crosshair, legend and other pointer-tied visuals. A hover repaints only the top canvas (~0.2 ms), and the browser composites two surfaces, not four.
  • Visible-range rendering — every renderer, the auto-scale and the indicator price range walk only the bars in view.
  • No per-frame garbage — viewport snapshots are cached between changes; number and date formatters are reused instead of rebuilt per label.
  • Auto-sized price axis — fitting the axis to its longest label costs ~0.05 ms and only relayouts when the width really changes.

Live ticks: incremental indicators

A tick only moves the forming bar, so indicators that implement update() recompute just that bar (SMA, EMA, WMA, VWMA, Bollinger, Envelope, RSI, MACD, ATR, OBV, Stochastic). Others fall back to a full recalculation. BB + EMA + RSI + MACD:

HistoryFull recalculationIncremental update()
20,000 bars~5 ms~0.0005 ms
100,000 bars~27 ms~0.001 ms

Custom indicators can opt in too — see Plugins → fast live updates.

Switching symbol or timeframe

  • Cheap full loads — indicator values live in an IndicatorValueMap (array-backed, ~3× cheaper to build than a timestamp-keyed Map), and setData reuses already-valid bars instead of copying them.
  • Loading only when it is slow — the previous chart stays up; a veil appears only if a switch outlasts 200 ms, so a normal ~130 ms network switch never flashes.
  • No stale data — superseded history requests are dropped and old sockets detached, so the last click always wins.
  • Local resampling — static data switches timeframe without refetching; slow resamples paint the veil before the main thread gets busy.

LTTB downsampling

Line and area charts downsample the visible range to ~2 points per pixel with Largest-Triangle-Three-Buckets when there are far more bars than pixels — the line stays visually identical while drawing dozens of times fewer points. A no-op at normal zoom. The algorithm is exported for your own use:

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)
Visible points → 1600Time / frameThroughput
10,000~0.025 ms39,600 / s
100,000~0.32 ms3,100 / s
1,000,000~2.6 ms380 / s

Indicators zoomed out

Below a pixel per bar, indicator lines, bands and histograms draw one span per pixel column: the column's lowest to highest point, joined to the column before, as wide as the line. It looks much the same as a stroke through thousands of points for a fraction of the raster work. Zoomed out on 200,000 bars with Bollinger Bands, EMA, RSI and MACD, a frame went from about 54 ms to about 21 ms on integrated graphics. node scripts/bench-render.mjs runs these numbers on your own machine.

WebGL renderer (preview)

renderer: 'webgl' draws the plot and indicator panes with WebGL 2, on a canvas under the 2D scene: the grid, sessions, candles and volume directly, and the Canvas 2D drawing of indicators, compare lines and most chart types recorded as GPU strokes, fills and rectangles, antialiased at their edges as Canvas 2D is. Text, drawings, orders, axes and the crosshair stay on Canvas 2D, as does anything the GPU wouldn't draw the same (left to Canvas 2D in order, so the stacking doesn't change); custom indicator plugins come along without changes. Candles match Canvas 2D to within 2/255; lines and fills differ only on a few antialiased edge pixels. The WebGL code is a chunk of its own (about 17 KB gzipped), loaded on first use; where WebGL 2 is missing, or its context is lost, the chart carries on with 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

Frame time while panning, on integrated graphics (Intel UHD; 16.7 ms is 60 fps):

ChartPixel ratioCanvas 2DWebGL
1600×900, 500 candles + 4 indicators227.4 ms19.6 ms
1600×900, zoomed out on 200,000 bars + 4 indicators234.5 ms20.2 ms
Six charts, 500 candles and two indicators each223.5 ms17.2 ms
2560×1400, 2,000 candles + 4 indicators141.6 ms17.7 ms
2560×1400, 2,000 candles + 4 indicators1.570.8 ms17.6 ms
2560×1400, 2,000 candles + 4 indicators2114.5 ms29.1 ms
2560×1400, 2,000 candles233.1 ms20.9 ms

Most WebGL frames above land on 16.7 ms; the averages carry a few longer ones. At a pixel ratio of 2 on a 2560×1400 chart, the browser's compositing of the full-size layers alone takes about 23 ms on this GPU. node scripts/bench-render.mjs --renderer=webgl runs these numbers on your own machine.

Off the main thread

IndicatorWorkerHost runs indicator math in a Web Worker with a Promise-based calculate(), a per-request timeout, and a synchronous fallback for SSR and tests.