Rendering 100k Data Points at 60fps with Chart.ts WebGL
How Chart.ts handles massive datasets. Triple-renderer architecture, automatic WebGL switching, and GPU-accelerated 3D charts.
Most charting libraries hit a wall around 10,000 data points. The frame rate drops. The browser tab freezes during zoom or pan. Tooltips lag. The experience degrades from "interactive visualization" to "unresponsive rectangle."
Chart.ts was designed from the start to handle datasets at every scale. Small datasets get SVG with full DOM access and CSS styling. Mid-range datasets switch to Canvas. Large datasets activate WebGL with GPU-accelerated rendering. The API is identical across all three renderers.
This post explains the architecture behind that capability and shows you how to use it.
The triple-renderer architecture
Chart.ts has three rendering backends:
| Renderer | Data points | Frame budget | Best for |
|---|---|---|---|
| SVG | 0 - 5,000 | 16ms | Accessibility, CSS, SSR, print |
| Canvas | 5,000 - 50,000 | 8ms | Interactive dashboards |
| WebGL | 50,000+ | 4ms | Large datasets, 3D, streaming |
These are not three separate libraries. They share the same layout engine, the same scale calculations, the same animation system, and the same public API. The only thing that changes is how pixels reach the screen.
How automatic switching works
By default, Chart.ts measures your dataset size and picks the best renderer:
import { createChart } from "@chartts/core"
// 500 points - renders as SVG
const chart1 = createChart("#container", {
type: "scatter",
data: generate(500),
x: "timestamp",
y: "value",
})
// 25,000 points - automatically switches to Canvas
const chart2 = createChart("#container", {
type: "scatter",
data: generate(25_000),
x: "timestamp",
y: "value",
})
// 100,000 points - automatically switches to WebGL
const chart3 = createChart("#container", {
type: "scatter",
data: generate(100_000),
x: "timestamp",
y: "value",
})The API is identical in all three cases. You pass data, declare axes, and the chart handles the rest. The switching thresholds are configurable:
const chart = createChart("#container", {
type: "line",
data: largeDataset,
x: "time",
y: "value",
renderer: {
svgThreshold: 2_000, // Switch from SVG to Canvas at 2k
canvasThreshold: 30_000, // Switch from Canvas to WebGL at 30k
},
})Manual renderer selection
Sometimes you want to force a specific renderer. Maybe you need SVG for a PDF export, or Canvas for consistent cross-browser rendering.
const chart = createChart("#container", {
type: "line",
data: dataset,
x: "time",
y: "value",
renderer: "webgl", // Force WebGL regardless of data size
})Valid values are "svg", "canvas", "webgl", and "auto" (the default).
Benchmark numbers
We benchmarked Chart.ts against common charting libraries using a scatter plot with varying data sizes. Tests ran on a 2024 MacBook Pro (M3 Pro) in Chrome 125. Numbers represent time from createChart() to first paint.
| Data points | Chart.ts (auto) | Chart.js | ECharts | Plotly |
|---|---|---|---|---|
| 1,000 | 8ms (SVG) | 12ms | 18ms | 45ms |
| 10,000 | 14ms (Canvas) | 89ms | 62ms | 210ms |
| 50,000 | 22ms (Canvas) | 450ms | 180ms | 890ms |
| 100,000 | 35ms (WebGL) | 1,200ms | 340ms | 2,100ms |
| 500,000 | 85ms (WebGL) | crash | 1,800ms | crash |
| 1,000,000 | 160ms (WebGL) | crash | crash | crash |
At 100k points, Chart.ts renders in 35ms. That is well under the 16ms frame budget for a single frame, meaning interactive zoom and pan remain smooth at 60fps after initial render.
At 1 million points, Chart.ts completes initial render in 160ms. Other libraries either crash the browser tab or become completely unresponsive.
What makes WebGL fast
WebGL performance comes from parallelism. A modern GPU has thousands of shader cores. When you render 100,000 scatter points on the CPU (Canvas), the browser iterates through each point sequentially, computing position and drawing a circle. That is 100,000 sequential operations.
With WebGL, Chart.ts uploads the point positions to GPU memory as a vertex buffer. The GPU processes all 100,000 points simultaneously across its shader cores. The vertex shader positions each point. The fragment shader colors it. The entire operation completes in a single draw call.
Chart.ts optimizes further with:
- Vertex buffer pooling. Reuses GPU memory allocations across chart updates instead of allocating and freeing on every render.
- Instanced rendering. For scatter plots, a single circle geometry is drawn 100,000 times with different positions and colors, using one draw call.
- Level-of-detail. When zoomed out, the chart reduces point density. If 50 data points occupy the same pixel, only one is rendered.
- Frustum culling. Points outside the visible viewport are skipped entirely during pan and zoom.
WebGL with React
The React API works exactly the same way. The renderer is transparent to your component code.
import { ScatterChart } from "@chartts/react"
export function LargeScatter({ data }: { data: Point[] }) {
return (
<ScatterChart
data={data}
x="timestamp"
y="value"
color="category"
className="h-96"
tooltip
zoom
pan
/>
)
}Pass 500 points and you get SVG. Pass 500,000 points and you get WebGL. The component API does not change.
3D charts with @chartts/gl
For true 3D visualization, @chartts/gl extends Chart.ts with GPU-accelerated chart types:
import { Scatter3D } from "@chartts/gl/react"
export function MultiVariateAnalysis({ data }: { data: DataPoint[] }) {
return (
<Scatter3D
data={data}
x="feature1"
y="feature2"
z="feature3"
color="cluster"
size="importance"
className="h-[500px]"
orbit
/>
)
}@chartts/gl provides six 3D chart types: Scatter3D, Surface3D, Bar3D, Globe3D, Map3D, and Line3D. All use WebGL exclusively and support orbit controls for rotation, zoom, and pan in three dimensions.
3D charts handle large datasets efficiently. A Scatter3D with 200,000 points renders at 60fps with orbit interaction. Surface3D renders grids up to 1000x1000 (1 million vertices) smoothly.
Streaming large datasets
Real-time data is where performance matters most. When new data arrives every 100ms, the chart cannot afford a 200ms render cycle. Chart.ts streaming mode uses incremental updates instead of full redraws.
import { createStreamingChart } from "@chartts/core"
const chart = createStreamingChart("#container", {
type: "line",
x: "time",
y: "value",
maxPoints: 100_000,
renderer: "webgl",
})
// Each append() only updates the GPU buffer incrementally
websocket.onmessage = (event) => {
const point = JSON.parse(event.data)
chart.append(point)
}The append() method adds a single data point to the end of the GPU vertex buffer. It does not re-upload the entire dataset. This makes the per-frame cost O(1) instead of O(n), regardless of how many points are already in the chart.
When maxPoints is set, old data is removed from the beginning of the buffer using a ring buffer strategy. No memory allocation, no garbage collection pressure.
Memory management
Large datasets require attention to memory. Chart.ts provides tools to monitor and control GPU memory usage:
const chart = createChart("#container", {
type: "scatter",
data: massiveDataset,
x: "x",
y: "y",
renderer: "webgl",
})
// Check GPU memory usage
const memory = chart.getMemoryUsage()
// { vertices: 4_800_000, indices: 1_200_000, textures: 0, totalBytes: 24_000_000 }
// Release GPU resources when done
chart.destroy()The destroy() method is important. WebGL resources are not garbage collected by the JavaScript engine. If you remove a chart from the DOM without calling destroy(), the GPU memory leaks. In React, @chartts/react handles this automatically in the component's cleanup effect.
When to use each renderer
Use SVG when you need DOM events on individual elements, CSS styling with pseudo-classes, server-side rendering, print output, or accessibility (screen readers can traverse SVG nodes).
Use Canvas when you have 5,000 to 50,000 data points and need interactive performance. Canvas is also more consistent across browsers for pixel-perfect rendering.
Use WebGL when you have 50,000+ data points, need 3D visualization, or are streaming data in real time. WebGL has the highest initial setup cost (shader compilation, buffer allocation) but the lowest per-frame cost at scale.
Use auto (the default) when you do not want to think about it. Chart.ts measures your data and picks the right renderer. This is the correct choice for 90% of applications.
Conclusion
Handling 100,000 data points should not require a different library, a different API, or a PhD in computer graphics. Chart.ts gives you a single API that scales from 10 data points to 1 million, automatically choosing the right rendering technology for the job. The same code that renders a 50-point SVG sparkline in a dashboard card can render a 500,000-point WebGL scatter plot in a data analysis tool.