Dynamic data, streaming and performance
Push and shift points without re-creating the chart, use update modes that skip the work you do not need, and keep a live chart smooth.
Mutate, then update
const chart = new Chart(canvas, {
type: 'line',
data: { labels: [], datasets: [{ label: 'Requests', data: [], tension: 0.3 }] },
options: { animation: false, responsive: true, maintainAspectRatio: false, parsing: false }
});
// The supported pattern: change the data, then tell the chart.
function pushPoint(label, value) {
chart.data.labels.push(label);
chart.data.datasets[0].data.push(value);
// Keep a fixed window so memory and render cost stay constant.
const WINDOW = 120;
if (chart.data.labels.length > WINDOW) {
chart.data.labels.shift();
chart.data.datasets[0].data.shift();
}
chart.update('none'); // no animation, no y-axis rescale
}
// Update modes and what each one skips
// chart.update() full update: re-parse, rescale, re-render, animate
// chart.update('none') re-render without animation
// chart.update('resize') the container changed size
// chart.update('reset') replay animations from the start
// chart.update('show') / ('hide') reveal or hide the whole chart
// chart.update('active') re-render the hover state only
// Adding a whole dataset
chart.data.datasets.push({ label: 'Errors', data: errorWindow, borderColor: '#ef4444' });
chart.update();
// Removing one: also remove the legend entry and any custom plugin state
chart.data.datasets.splice(1, 1);
chart.update();| Situation | Call | Why |
|---|---|---|
| A point arrived from a socket | update('none') | Animation on a live chart is noise |
| The user changed a filter | update() | A transition helps the change read |
| Only the hover state changed | update('active') | Cheapest possible re-render |
| The container was resized | resize() | Recomputes the layout and the device ratio |
| Sweeping a whole new dataset in | Replace the array, then update() | Reuse the dataset object |
| High-frequency data | Batch per animation frame | Never update per message |
💡
Chart.js 4 has a default animation of about one second on
update(). On a chart that receives data every second, that animation never finishes and the result is a permanently lagging, wobbly line. Set animation: false for anything streaming.Streaming without dropping frames
// Batch incoming points and render at most once per frame.
class StreamBuffer {
constructor(chart, { window = 200, datasetIndex = 0 } = {}) {
this.chart = chart;
this.window = window;
this.datasetIndex = datasetIndex;
this.pending = [];
this.frame = null;
}
push(point) {
this.pending.push(point);
// requestAnimationFrame coalesces a burst into a single render.
if (this.frame === null) this.frame = requestAnimationFrame(() => this.flush());
}
flush() {
this.frame = null;
if (!this.pending.length) return;
const dataset = this.chart.data.datasets[this.datasetIndex];
for (const point of this.pending) dataset.data.push(point);
this.pending.length = 0;
if (dataset.data.length > this.window) {
dataset.data.splice(0, dataset.data.length - this.window);
}
this.chart.update('none');
}
stop() {
if (this.frame !== null) cancelAnimationFrame(this.frame);
this.frame = null;
this.pending.length = 0;
}
}
const buffer = new StreamBuffer(chart, { window: 300 });
socket.addEventListener('message', (event) => buffer.push(JSON.parse(event.data)));// Performance options on the chart itself
const options = {
animation: false,
responsive: true,
maintainAspectRatio: false,
parsing: false, // the data is already {x, y}
normalized: true, // the data is sorted by x
spanGaps: false,
elements: {
point: { radius: 0, hitRadius: 6 }, // no visible points on a live line
line: { borderWidth: 1.5 }
},
plugins: {
decimation: {
enabled: true,
algorithm: 'lttb', // Largest-Triangle-Three-Buckets
samples: 500, // target points to keep
threshold: 1000 // only decimate above this many points
},
legend: { display: false }
},
scales: {
x: { type: 'linear', ticks: { maxTicksLimit: 6, autoSkip: true } },
y: { beginAtZero: false, ticks: { maxTicksLimit: 5 } }
}
};
// Decimation must be registered before it can be used:
// import { Decimation } from 'chart.js';
// Chart.register(Decimation);parsing: falseis the single biggest win for a large series: Chart.js stores your objects instead of walking them to build an internal form.normalized: truetells Chart.js the data is already sorted, which skips the internal sort. Unsorted data with this flag produces a scrambled line.- Decimation keeps the shape of a spike rather than averaging it away, which matters for monitoring data. It does not reduce the stored array — only what is drawn.
- A zero point radius is what makes a 1,000-point line look like a line rather than a caterpillar.
Duplicate charts and other leaks
// A chart instance holds listeners and a ResizeObserver. Destroying is not optional.
// The wrong pattern: recreate on every data change.
function renderWrong(data) {
new Chart(canvas, { type: 'line', data }); // a new instance every call
}
// The right pattern: create once, update many times.
let chart = null;
function render(data) {
if (!chart) {
chart = new Chart(canvas, { type: 'line', data, options });
} else {
chart.data = data;
chart.update();
}
}
// Teardown when the chart is no longer needed
function destroyChart() {
chart?.destroy(); // removes listeners, the observer and the canvas state
chart = null;
}
// Detecting the mistake: Chart.js warns when an instance already exists for a
// canvas. The warning is easy to miss, and the visible symptom is a chart that
// animates twice as fast or a doubling of the hover tooltip.| Symptom | Cause | Fix |
|---|---|---|
| Animations look doubled | Two instances on one canvas | destroy() before re-creating, or reuse |
| Memory grows on navigation | Instances kept after the element is gone | Destroy in the component's teardown |
| Hover highlights the wrong points | Stale dataset references after mutating arrays | Update through chart.data |
| The chart flickers on each update | Animation on a streaming chart | animation: false |
| A CPU spike every second | One update() per message | Batch into a requestAnimationFrame |
| The line jumps when old points fall off | beginAtZero or a rescaling axis | Fix min and max, or use suggestedMin |
// A stable y axis: the line stops jumping when a new maximum arrives.
const stableAxis = {
scales: {
y: {
// A fixed scale is easier to read than an auto-scaling one, but you must
// choose the bounds. A rolling window is a good compromise.
suggestedMin: 0,
suggestedMax: 1000,
ticks: { maxTicksLimit: 5 }
}
}
};
// Or compute the window from the data with hysteresis, so the axis only grows
// when the data really needs it.
let axisMax = 100;
function updateAxis(chart) {
const peak = Math.max(...chart.data.datasets[0].data.map((p) => p.y ?? p), 0);
if (peak > axisMax) axisMax = Math.ceil(peak * 1.1 / 100) * 100; // grow in steps
chart.options.scales.y.max = axisMax;
}FAQ
Why does update() feel slow?
The default update re-parses the data, recomputes the scales and animates the change. For a live chart set
animation: false and parsing: false, use update('none'), and batch points into a single animation frame instead of calling update per message.How do I keep a fixed-size scrolling window?
Push the new point and shift from the front once the array exceeds your limit, then call
update('none'). Keep the same array and dataset objects — replacing them forces a full re-parse.Related
Responsive canvas layout and image export Testing charts and migrating to v4
Last refreshed 2026-09-18.