Chart.js update(): Reactive Data Changes Without Full Canvas Re-Renders
Modify Chart.js data arrays in place and call chart.update() to animate transitions without destroying the canvas. A practical look at animation modes, streaming windows, and the performance limits of frequent updates.
16 Jun 2026, 17:15 UTC

The flicker problem
When a Chart.js visualization needs new data, the instinct is often to destroy the existing chart and build a new one. The canvas flickers, the axes rebuild from scratch, and if the update fires on a timer, the page starts to feel heavy. There is a lighter path: change the data already bound to the chart and tell Chart.js to redraw.
Mutate the model, not the DOM
Chart.js keeps a data model on the chart instance. You can reach into chart.data.datasets[0].data and change the values directly. The canvas does not change until you call chart.update(). That method triggers a transition from the old values to the new ones, reusing the same canvas element and scale objects.
This separation matters because allocating a new canvas context and rebuilding scales is expensive. Doing it on every data tick wastes CPU and can leak memory if the old chart instance was not properly destroyed.
A minimal update loop
The following example assumes Chart.js v4.x is already loaded on the page. Run this in the browser console or a script tag after the canvas element exists.
// Cache the instance so you never create a second one
const ctx = document.getElementById('myChart').getContext('2d');
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: Array.from({length: 10}, (_, i) => `T-${10-i}`),
datasets: [{
label: 'Requests',
data: Array.from({length: 10}, () => Math.floor(Math.random() * 100)),
borderColor: '#36A2EB',
tension: 0.3
}]
},
options: { responsive: true, animation: true }
});
// Every second, slide the window forward
setInterval(() => {
const ds = chart.data.datasets[0].data;
ds.shift(); // drop oldest
ds.push(Math.floor(Math.random() * 100)); // add newest
chart.data.labels.shift();
chart.data.labels.push(`T-${Date.now()}`);
chart.update(); // animate to new values
}, 1000);
Expected check: the line should glide to the new point on the right and drop the leftmost point without the canvas disappearing. If the line does not move, confirm that chart.update() is actually being called after the mutation.
Animation modes: when to skip the transition
update() accepts an optional mode string that controls what happens during the redraw. For most data changes, the default animated transition is desirable. For high-frequency streams or programmatic resets, it can be counterproductive.
| Mode | Behavior | Typical use |
|---|---|---|
undefined | Animates data points and scales using configured durations | Normal reactive updates |
'none' | Redraws immediately with no animation | Rapid polling or when animation causes stutter |
'resize' | Recalculates layout and scale sizes | Container size changes without data mutation |
'active' | Updates active (hover) elements | Interaction-driven redraws, not bulk data shifts |
To see the difference, replace chart.update() with chart.update('none') in the interval above. The line will jump to the new value instantly. Neither behavior is wrong; the choice depends on whether the eye needs the transition or the CPU needs the break.
Streaming windows and reference stability
The push/shift pattern above keeps a fixed number of visible points. It is more efficient than assigning a brand-new array to dataset.data on every tick, because Chart.js can work with the existing array structure rather than replacing the reference wholesale. That said, if your source data arrives as a nested object, map it to a flat array of numbers before assigning it. Passing a deeply nested structure directly can cause reference errors or silent rendering failures.
If you are driving the chart from an external state manager, store the Chart instance in a variable or a ref, not in component state that triggers re-renders. Re-initializing the chart inside a render or effect loop is the most common source of redundant canvas instances and memory leaks.
Trade-offs and limits
Calling update() is cheaper than rebuilding the chart, but it is not free. Each call recalculates scales, evaluates the dataset geometry, and paints the canvas. Running the interval above every 16 milliseconds will likely cause CPU spikes and dropped frames, even with update('none'). Throttle the data source or batch multiple points into a single update.
Another limitation: update() animates the transition using the options present on the instance. If you need to change the animation duration or easing dynamically, you must modify chart.options.animation before calling update().
Practical verification
After wiring up an update loop, open your browser's performance profiler and record a few seconds. Look for repeated Chart constructor calls or growing heap snapshots; either suggests that a new instance is being created instead of the cached one being updated. Visually, confirm that the canvas element itself never detaches from the DOM during updates.
Because Chart.js has changed internals between major versions, verify the exact update() signature and mode strings against the documentation for the version you have installed. The behavior described here reflects the stable API across recent v3 and v4 releases, but edge cases around custom plugins or scale types may differ.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.