Installing Chart.js and tree-shaking the bundle
Choose between chart.js/auto and explicit registration, handle the ESM-only packaging, and measure what the explicit path actually saves.
Setup paths
npm install chart.js
# only for a time or timeseries axis
npm install chartjs-adapter-date-fns date-fns<div style="position: relative; height: 360px; width: 100%;">
<canvas id="revenue"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<script>
const chart = new Chart(document.getElementById('revenue'), {
type: 'bar',
data: { labels: ['Q1', 'Q2', 'Q3', 'Q4'], datasets: [{ label: 'Revenue', data: [820, 932, 901, 1290] }] },
options: { responsive: true, maintainAspectRatio: false }
});
</script>// chart.js/auto registers every controller, element, scale and plugin.
// Perfect for a prototype; it pulls in the whole library.
import Chart from 'chart.js/auto';
// The explicit path: import and register only what this chart uses.
import {
Chart, BarController, BarElement, CategoryScale, LinearScale,
Tooltip, Legend, Title, SubTitle
} from 'chart.js';
Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, Legend, Title, SubTitle);
export function createRevenueChart(canvas, data) {
return new Chart(canvas, {
type: 'bar',
data,
options: { responsive: true, maintainAspectRatio: false }
});
}| Path | Bundle impact | Fits |
|---|---|---|
| UMD script tag | Nothing in your bundle | Static pages, quick experiments |
chart.js/auto | The whole library | Prototypes, internal tools, one-off pages |
Named imports + Chart.register | Only the registered pieces | Production bundles |
| A wrapper package | Whatever it registers, plus the wrapper | When the framework lifecycle handling is worth it |
import Chart from 'chart.js' with no registration | Minimal, and nothing renders | Never — you will get an empty canvas |
⚠️
With the explicit path, an unregistered piece fails silently: the canvas stays blank and Chart.js logs nothing useful. If a chart does not draw, check the registration list before anything else — a missing
CategoryScale produces exactly the same symptom as a missing dataset.CommonJS, ESM and measuring
// Chart.js 4 ships ESM only. A CommonJS consumer needs a dynamic import.
// index.cjs — a Node tool or an old build pipeline
async function loadChartjs() {
const { Chart, BarController, BarElement, CategoryScale, LinearScale } =
await import('chart.js');
Chart.register(BarController, BarElement, CategoryScale, LinearScale);
return Chart;
}
// In the browser with a bundler, the static import is the normal route.
// In a test runner, set the environment to ESM or use dynamic import as above.
// A wrapper module keeps the registration in exactly one place, so no other
// file has to remember which pieces a bar chart needs.
// src/charts/register.js
import {
Chart, BarController, BarElement, LineController, LineElement, PointElement,
CategoryScale, LinearScale, TimeScale, Tooltip, Legend, Title, Filler
} from 'chart.js';
import 'chartjs-adapter-date-fns';
Chart.register(
BarController, BarElement, LineController, LineElement, PointElement,
CategoryScale, LinearScale, TimeScale, Tooltip, Legend, Title, Filler
);
export { Chart };
// Every chart module imports { Chart } from './register.js' and nothing else.# Measure, do not guess. Compare the two imports on the same chart.
npx esbuild src/charts/auto-entry.js --bundle --minify --format=esm --outfile=dist/auto.js
npx esbuild src/charts/explicit-entry.js --bundle --minify --format=esm --outfile=dist/explicit.js
ls -l dist/auto.js dist/explicit.js
gzip -c dist/auto.js | wc -c
gzip -c dist/explicit.js | wc -c
# Typical result for a single bar chart:
# auto ~210 KB minified, ~70 KB gzipped
# explicit ~95 KB minified, ~32 KB gzipped- The saving is real but bounded: the core, the tooltip and the legend are most of what remains. Adding a second and third chart type grows the bundle toward the auto total.
- Register globally once, at module scope. Registering inside a component means the registration runs on every component instance, which is wasted work and confusing to read.
- The adapter for a time axis is a separate dependency. Import it for its side effect — it registers itself — and only when a
timeortimeseriesscale is actually used. - Wrapper packages such as
react-chartjs-2importchart.js/autoby default in some versions. Check the wrapper's peer imports before assuming you are tree-shaking.
First-run pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| Blank canvas | A component was never registered | Import and register the controller, element and scales |
Chart is not a constructor | Imported the wrong entry point | Use chart.js/auto or import Chart from chart.js |
| Chart is 0px tall | The parent has no height | Wrap the canvas in a positioned element with an explicit height |
| The chart grows without bound | The canvas is a direct child of a block element | Give it a wrapper with position: relative and a fixed height |
| Type errors in TypeScript | Registering types is separate from registering values | Chart.js 4 ships its own types; no @types package is needed |
| Two charts on one canvas | The component was mounted twice | Track the instance and destroy before creating |
| Nothing appears in a hidden tab | The canvas was 0 by 0 at construction | Call chart.resize() when the tab becomes visible |
<!-- The canonical sizing wrapper. Chart.js measures the parent, so the parent
must have a resolvable height. -->
<div style="position: relative; height: 360px; width: 100%;">
<canvas id="revenue" role="img" aria-label="Revenue by quarter"></canvas>
</div>
<!-- A common mistake: a height on the canvas itself does nothing useful,
because Chart.js writes the canvas width and height attributes. -->
<canvas id="broken" height="360"></canvas>One more thing worth knowing before going further: Chart.js 4 ships TypeScript definitions built in. Installing @types/chart.js alongside it produces conflicting declarations, so remove that package if you find it in a lockfile.
FAQ
Is chart.js/auto worth avoiding?
For a single chart type in a production bundle, yes — explicit registration roughly halves the payload. For a dashboard that uses six chart types plus the zoom and annotation plugins, the saving shrinks and the auto import becomes the pragmatic choice.
Why is nothing rendered and no error logged?
Almost always a missing registration. The explicit path requires each controller, element, scale and plugin to be registered, and an unregistered piece is skipped without a warning rather than throwing.
Related
Setup and configuration Custom controllers, elements and plugins
Last refreshed 2026-09-18.