Optimizing Chart.js Performance with Data Decimation for Large Datasets
Chart.js's built-in decimation plugin cuts rendered points from 50k to ~500 using LTTB, preserving visual trends while restoring 60fps interaction. Requires 'index' scale and sorted data; tooltips need a callback for original values.
27 Jul 2025, 04:15 UTC

The Problem: Browser Charts Choke on Large Data
Rendering 50,000+ data points in a Chart.js line chart often drops frame rates below 30 fps, making interactions sluggish and increasing memory pressure. The browser must create path segments for every point, even when many pixels overlap. Downsampling on the server helps, but you lose the ability to zoom or pan client-side without re-fetching.
Chart.js Built-in Decimation Plugin
Chart.js includes a decimation plugin that reduces the number of points drawn on the canvas while preserving visual trends. It activates automatically when the x-axis scale type is set to 'index' and the dataset is sorted by x. The plugin supports three algorithms:
- lttb (Largest-Triangle-Three-Buckets) — preserves peaks, valleys, and outliers best.
- min-max — keeps the minimum and maximum y-value per bucket.
- mean — averages y-values per bucket.
LTTB is more computationally expensive than the others but delivers superior visual fidelity for volatile data like financial ticks or sensor readings.
Worked Example: 50,000-Point Line Chart with LTTB
The following configuration renders a performant line chart using the decimation plugin. Run this in any modern browser; no special permissions required. Ensure your data array is pre-sorted by the x-value (timestamp or index) before passing it to Chart.js.
const ctx = document.getElementById('chart').getContext('2d');
const rawData = []; // 50,000 points: [{x: 0, y: 12.3}, {x: 1, y: 12.5}, ...]
for (let i = 0; i < 50000; i++) {
rawData.push({x: i, y: Math.sin(i * 0.01) * 100 + Math.random() * 10});
}
new Chart(ctx, {
type: 'line',
data: {
datasets: [{
label: 'Sensor Reading',
data: rawData,
borderColor: 'rgb(54, 162, 235)',
borderWidth: 1,
pointRadius: 0, // hide points; decimation handles sampling
tension: 0.1
}]
},
options: {
responsive: true,
scales: {
x: {
type: 'index', // required for decimation
title: { display: true, text: 'Sample Index' }
},
y: {
title: { display: true, text: 'Value' }
}
},
plugins: {
decimation: {
algorithm: 'lttb',
samples: 500 // target number of points to render
},
tooltip: {
// show original value for the decimated index
callbacks: {
label: (ctx) => {
const original = rawData[ctx.dataIndex];
return `Original: ${original.y.toFixed(2)}`;
}
}
}
}
}
});
Key settings:
scales.x.type: 'index'— tells Chart.js to treat x as sequential indices, enabling the plugin.decimation.samples: 500— caps rendered points to ~500 regardless of dataset size.pointRadius: 0— avoids drawing individual points; the line connects decimated samples.
Trade-offs and Limitations
- Line charts only. Bar, scatter, and pie charts do not support decimation out of the box.
- Sorted data required. Unsorted input produces incorrect bucketing and visual artifacts.
- Tooltip precision. By default, tooltips show the decimated point's value. The callback above restores the original value for the sampled index, but you cannot recover the exact original point for every pixel without storing the full dataset separately.
- LTTB overhead. On extremely large datasets (>200k points), the LTTB pass itself may block the main thread. Consider web workers or server-side pre-decimation for those cases.
Verify the Gain
- Open Chrome DevTools → Performance tab.
- Record a 5-second interaction (pan, zoom, hover) with decimation disabled (comment out the plugin block).
- Record again with decimation enabled.
- Compare Scripting and Rendering times. Expect 60-80% reduction in frame scripting cost for 50k points.
If frame rates stay below 30 fps, reduce samples further or switch to min-max for lower CPU usage.
Next Steps
Add the decimation plugin to any Chart.js line chart that exceeds 10,000 points. Start with samples: 1000 and algorithm: 'lttb', then profile. For real-time streaming data, combine decimation with a sliding window that drops old points before they reach the plugin.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.