Chart.js Tree-Shaking: Cutting Bundle Size with Explicit Registration
Chart.js v3+ lets you import only the controllers, scales, and elements you need. Explicit registration via Chart.register() cuts a typical line chart bundle from ~80 kB to ~18 kB gzipped. This post shows the pattern, a Vite+TS worked example, and the runtime gotchas to watch for.
09 Sept 2025, 00:33 UTC

The problem: 100 kB for a single line chart
You add a simple line chart to a React or Vue dashboard. The bundle analyzer shows Chart.js contributing 80–100 kB minified and gzipped — even though you only use a category axis, a linear axis, points, and lines. The culprit is chart.js/auto, the default entry point that registers every controller, scale, element, and plugin. It’s convenient for prototypes, but it defeats dead-code elimination in Rollup, Webpack 5, Vite, and esbuild.
Chart.js v3 (2021) split the library into ~30 sub-packages. By importing only what you need and calling Chart.register(...) once at bootstrap, a minimal line chart drops to roughly 15–25 kB gzipped. This post walks through the registration pattern, a worked example with real bundle numbers, and the trade-offs you’ll hit in production.
How explicit registration works
The chart.js package entry (without /auto) exports only the core Chart class and registration helpers. Controllers, scales, elements, and plugins live in named exports that you import and register:
import {
Chart,
LineController,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
} from 'chart.js';
Chart.register(
LineController,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);Each import pulls in only that module’s code. Because chart.js has "sideEffects": false in its package.json (since v3.5) and a proper exports map, bundlers can safely drop everything else. TypeScript users also get granular types — importing LineController brings LineControllerDatasetOptions and LineControllerChartOptions, so config mismatches surface at compile time.
Worked example: minimal line chart in Vite + TypeScript
Create a fresh project to see the difference:
npm create vite@latest chart-demo -- --template react-tscd chart-demo && npm install chart.js- Replace
src/App.tsxwith a component that renders a line chart using the explicit imports above. - Run
npm run buildand inspectdist/assets/*.js. The Chart.js chunk should be ~18 kB gzipped.
Now swap the import to import Chart from 'chart.js/auto', remove the Chart.register call, rebuild, and compare. The same chunk typically grows to 60–100 kB gzipped because every controller (bar, bubble, polarArea, etc.), every scale (logarithmic, radialLinear, time), and every element (arc, bar) is pulled in.
Verify with npx vite-bundle-analyzer or webpack-bundle-analyzer. You’ll see BubbleController, PolarAreaController, RadialLinearScale, and others absent in the explicit build but present in the auto build.
Trade-offs and limitations
- Runtime registration errors. If you use a time axis but forget
import { TimeScale } from 'chart.js'; Chart.register(TimeScale);, the chart throws"Scale type 'time' is not registered"at runtime. No compile-time guard exists for string-based scale types. - Dynamic config strings defeat tree-shaking. Writing
config.type = 'bar'at runtime means the bundler cannot proveBarControlleris needed unless it’s statically imported. Prefer static imports for every type you’ll ever render. - Plugin compatibility. Community plugins (annotation, zoom, datalabels) follow the same pattern:
import { Annotation } from 'chartjs-plugin-annotation'; Chart.register(Annotation);. Check each plugin’s README for aregisterablesexport and peerDependency range — some still assume global registration and break with mismatched versions. - Mixing strategies duplicates code. Using
chart.js/autoin one entry point and manual imports in another pulls two copies of the core. Pick one strategy per bundle. - Team drift. Tutorials still show
chart.js/auto. New contributors may re-introduce it. Enforce the explicit pattern with an ESLint import alias or ano-restricted-importsrule.
Actionable migration checklist
- Replace
import Chart from 'chart.js/auto'with named imports from'chart.js'. - Add a single
Chart.register(...)call at app bootstrap (e.g.,main.tsxor a dedicatedchart-setup.ts). - Remove any global
Chart.defaultsmutations that relied on auto-registered defaults. - Run the bundle analyzer; confirm unused controllers/scales are absent.
- Add a lint rule to forbid
chart.js/auto. - When upgrading major versions (v4 kept the registration API stable but renamed internal classes), verify peerDependency ranges of every plugin.
Explicit registration is a small upfront investment that pays off in every production build. The pattern is stable, well-supported by modern bundlers, and gives you both smaller bundles and better TypeScript safety.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.