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 }
  });
}
PathBundle impactFits
UMD script tagNothing in your bundleStatic pages, quick experiments
chart.js/autoThe whole libraryPrototypes, internal tools, one-off pages
Named imports + Chart.registerOnly the registered piecesProduction bundles
A wrapper packageWhatever it registers, plus the wrapperWhen the framework lifecycle handling is worth it
import Chart from 'chart.js' with no registrationMinimal, and nothing rendersNever — 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 time or timeseries scale is actually used.
  • Wrapper packages such as react-chartjs-2 import chart.js/auto by default in some versions. Check the wrapper's peer imports before assuming you are tree-shaking.

First-run pitfalls

SymptomCauseFix
Blank canvasA component was never registeredImport and register the controller, element and scales
Chart is not a constructorImported the wrong entry pointUse chart.js/auto or import Chart from chart.js
Chart is 0px tallThe parent has no heightWrap the canvas in a positioned element with an explicit height
The chart grows without boundThe canvas is a direct child of a block elementGive it a wrapper with position: relative and a fixed height
Type errors in TypeScriptRegistering types is separate from registering valuesChart.js 4 ships its own types; no @types package is needed
Two charts on one canvasThe component was mounted twiceTrack the instance and destroy before creating
Nothing appears in a hidden tabThe canvas was 0 by 0 at constructionCall 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.

Setup and configuration Custom controllers, elements and plugins

Last refreshed 2026-09-18.