Skip to content

Performance ​

Charturbo keeps zoom, pan and hover at display rate by doing the heavy work in WebAssembly and keeping data on the GPU. This page explains where the speed comes from, how to keep it with large data, and what the benchmarks measured.

Benchmark comparison

How it stays fast ​

TechniqueWhere it applies
M4 decimation per pixel column (first, last, min and max of each column) in RustLine and area charts: a million samples become a few thousand segments, and spikes are kept
Pixel-column OHLC aggregationCandlesticks narrower than 2 px
GPU instancing, points uploaded onceScatter / bubble charts: zoom and pan only change the view
Value grid as one R32Float texture with a 256-color lookup tableHeatmaps up to 4096 × 4096 cells: one quad, color map changes without re-upload
Hit-testing, stacking, tick layout, hierarchy and Sankey layout in RustHover and layout cost stays out of JavaScript
Upload only what changedStreaming uploads the decimated result (about 155 KB per frame for a 1M window); Sankey hover and treemap legend highlighting change a uniform instead of geometry
GPU-resident graph layout (stepPhysicsAsync, WebGPU)Positions and velocities stay in GPU buffers; the GPU builds the Barnes-Hut octree and integrates
DOM only for textTitle, legend, axis labels and tooltips; tick label DOM is rebuilt only when ticks change

Tips for large data ​

Pass typed arrays ​

Use Float32Array for values and Float64Array for x values and timestamps. Plain arrays are accepted but converted first. For a single series given as a Float32Array (and an optional Float64Array x), the SDK passes the array to the engine without making a concatenated copy; in the benchmarks this cut the WebAssembly memory of a 10M-sample line from 78 MB to 41 MB.

ts
const values = new Float32Array(n);
const x = new Float64Array(n); // epoch ms for a time axis
sdk.renderLineChart([{ name: 'Sensor', values, x }], { axes: { x: { type: 'time' } } });
sdk.render();

Append instead of re-rendering ​

appendSeriesData adds samples to an existing series and maxPoints keeps a sliding window. Trimming is amortized O(appended samples), and the next frame re-decimates only the visible range. appendAreaData does the same for stacked areas. Calling renderLineChart with the full data on every update copies everything again.

ts
sdk.appendSeriesData(0, chunk, chunkX, { maxPoints: 1_000_000 });
sdk.render();

Render only when something changed ​

There is no idle loop: render() draws one frame. Call it after data or view changes, or use autoRender: true on enableNodeInteractions. For streaming, batch updates and render once per animation frame.

Share a GPU device across many charts ​

Charturbo.create(canvas) requests an adapter and device and compiles every pipeline per canvas, and browsers cap WebGL contexts at about 16. With CharturboDevice, 100 sparklines took 165 ms on WebGPU and 717 ms on WebGL2 to create and first render (headless Chrome). See Shared GPU Device.

Know the limits ​

  • Scatter charts have no point decimation: every point is drawn. At 5M points zoom and pan were GPU-bound at 12–13 fps.
  • Animations are skipped when a chart stages more than 50,000 marks (for example a 1M-point scatter).
  • Graph physics at 100k nodes is not real time on the CPU. Lay out first with warmup() and then only render, step less often, or use stepPhysicsAsync on WebGPU.
  • On the WebGL2 fallback, peak WebAssembly memory was 1.6–2.4× higher for scatter, heatmap and multi-series charts.

Measure ​

sdk.getRenderStats() returns GPU buffer and texture bytes, the bytes uploaded by the last frame, staged mark counts and the WebAssembly memory size (null on backends without counters).

Benchmark summary ​

Measured with the web/bench/ harness on one machine: Apple M1, headless Chrome 156, 960 × 540 canvas at DPR 1, 60 Hz. Steady state is 3 seconds of wheel zoom and drag pan (hover sweep for bars). Numbers will differ on other machines and browsers.

ScenarioTTFF ms WebGPU / WebGL2Steady frame med / p95 / max msDropped vsyncs / framesWork per frame med msGPU MB
line-1m-1s16.7 / 16.616.7 / 16.7 / 16.70/1801.70.7
line-10m-1s91.8 / 92.316.7 / 16.7 / 33.34/1762.90.7
area-stacked-1m26.9 / 25.616.7 / 16.7 / 33.31/1795.55.3
scatter-1m46.1 / 46.916.7 / 16.7 / 16.70/1801.232.7
scatter-5m210 / 24483.3 / 117 / 117144/371.6128.7
bar-10k19.0 / 19.116.7 / 16.7 / 16.70/1801.11.2
heatmap-409692.9 / 96.416.7 / 16.7 / 16.70/1801.364.7
candle-1m33.2 / 33.316.7 / 16.7 / 16.70/1801.60.7
stream-1m15.9 / 16.216.7 / 16.7 / 16.70/1803.20.7

TTFF and frame times are quantized to the 16.7 ms vsync; 16.7 means "within one frame". stream-1m appends 10k points per frame to a 1M window.

Graph layout, 3-second steady state (2026-10-27, before the layout became GPU-resident):

Nodes / edgesCPU physics ms / FPSHybrid GPU physics ms / FPS
10k / 20k17.74 / 52.76.23 / 60.0
100k / 200k285.76 / 3.741.87 / 19.6

Library comparisons (uPlot, ECharts, Chart.js, Plotly, ChartGPU, SciChart, Highcharts, three.js, deck.gl, cosmos.gl, 3d-force-graph), the full method and the known limits are in Benchmark Methodology. Run them yourself with cd web && npm run bench.

See also ​

MIT License