Chart.js 4 Modular Imports: Cutting Bundle Size with Tree-Shaking
Chart.js 4 removes auto-registration. Import only the controllers, scales, elements, and plugins you need, register them explicitly, and let your bundler tree-shake the rest. A minimal line chart with decimation drops to ~20 kB gzipped.
21 Dec 2025, 07:02 UTC

The Problem: Chart.js Bundle Bloat
Adding a single line chart to a modern frontend often pulls in 80–120 kB of minified JavaScript — controllers for every chart type, all scale implementations, and plugins you never use. In Chart.js 3, the 'chart.js/auto' entry point registered everything by default. Chart.js 4 removes that auto-registration entirely, making tree-shaking the default path for size optimization.
Useful Takeaway
Import only the core Chart class plus the specific controllers, scales, elements, and plugins your charts actually need. Register them explicitly. With a modern bundler (Vite, Webpack 5+, Rollup, esbuild) in production mode, this typically reduces the Chart.js portion of your bundle to 15–30 kB for a basic cartesian chart.
How the Modular Architecture Works
Chart.js 4 ships as pure ESM. Every controller (LineController, BarController, etc.), scale (CategoryScale, LinearScale, RadialLinearScale), element (PointElement, LineElement, BarElement), and plugin (Tooltip, Legend, Title, Filler, DecimationPlugin) lives in its own module. The Chart.register() method accepts any number of these components and makes them available by their string identifiers (e.g., 'line', 'category').
Tree-shaking works because each module is marked side-effect-free. When you import only what you reference, the bundler drops the rest. The UMD build (chart.umd.js) still exists for script-tag usage but defeats this optimization — avoid it in bundled applications.
Worked Example: Minimal Line Chart with Decimation
Below is a complete, production-ready setup for a line chart that handles 50 k data points using the decimation plugin. Assumes Chart.js 4.4+ and chartjs-plugin-decimation 2.0+.
Install Dependencies
npm i chart.js@^4 chartjs-plugin-decimation@^2
Run in your project root. Requires write access to node_modules and package.json. No elevated permissions needed.
Chart Module (TypeScript)
// src/charts/lineChart.ts
import {
Chart,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
LineController
} from 'chart.js';
import { DecimationPlugin } from 'chartjs-plugin-decimation';
// Register only what this chart uses
Chart.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
LineController,
DecimationPlugin
);
export function createLineChart(
canvas: HTMLCanvasElement,
labels: string[],
data: number[]
): Chart {
return new Chart(canvas, {
type: 'line',
data: {
labels,
datasets: [{
label: 'Metric',
data,
borderColor: 'rgb(59, 130, 246)',
tension: 0.2,
// Decimation options — plugin must be registered above
decimation: {
algorithm: 'lttb',
samples: 500
}
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: { display: true, text: 'High-Density Series' },
decimation: { enabled: true }
},
scales: {
x: { type: 'category' },
y: { type: 'linear' }
}
}
});
}
Key points: LineController is required for type: 'line'. Forgetting it throws "line" is not a registered controller at runtime. DecimationPlugin comes from a separate package and must be registered like any other plugin. The decimation dataset option and plugin option both enable the feature.
Usage in a Component (Framework-Agnostic)
// src/components/ChartView.ts
import { createLineChart } from '../charts/lineChart';
import { onMounted, onUnmounted, ref } from 'vue'; // or React useEffect, etc.
export function useLineChart() {
const canvasRef = ref();
let chartInstance: ReturnType | null = null;
const mount = (labels: string[], data: number[]) => {
if (!canvasRef.value) return;
chartInstance = createLineChart(canvasRef.value, labels, data);
};
const destroy = () => {
chartInstance?.destroy();
chartInstance = null;
};
onMounted(() => mount(generateLabels(50000), generateData(50000)));
onUnmounted(destroy);
return { canvasRef };
}
function generateLabels(n: number) { return Array.from({ length: n }, (_, i) => `Point ${i}`); }
function generateData(n: number) { return Array.from({ length: n }, () => Math.random() * 100); }
Verifying the Bundle Reduction
Run a production build and inspect the output. With Vite:
npm run build -- --mode production
npx vite-bundle-analyzer dist
Run in project root. Requires vite-bundle-analyzer installed as a dev dependency. The analyzer opens a browser tab showing a treemap. Search for CategoryScale, LineController, DecimationPlugin — they should appear. Confirm BarController, PolarAreaController, RadialLinearScale are absent. The Chart.js core chunk should be roughly 15–30 kB gzipped depending on minification.
Expected check: No console errors on chart render. Interaction (hover, zoom, pan) remains smooth at 50 k points. If you see "line" is not a registered controller, you missed LineController in Chart.register().
Limits and Common Mistakes
1. Mixing UMD and ESM
Including <script src="https://cdn.jsdelivr.net/npm/chart.js"> alongside ESM imports duplicates the entire library and breaks registration (two Chart globals). Pick one: either full ESM with bundler, or UMD global only.
2. Framework Wrapper Version Mismatch
Wrappers like react-chartjs-2, vue-chartjs, ng2-charts re-export Chart.js internals. Their peer dependency must match your chart.js version exactly. A mismatch causes duplicate registrations or missing types. Check npm ls chart.js — only one version should appear.
3. Forgetting Scale Imports for Non-Cartesian Charts
Radar, polar area, and doughnut charts need RadialLinearScale (and ArcElement for doughnut). Cartesian charts need CategoryScale and/or LinearScale, LogarithmicScale, TimeScale, TimeSeriesScale as appropriate.
4. Decimation Plugin Not in Core
chartjs-plugin-decimation is a separate package. It does not tree-shake into core. Install and register it explicitly. Older decimation plugins (pre-v2) are incompatible with Chart.js 4.
5. TypeScript Module Resolution
Set "moduleResolution": "bundler" in tsconfig.json for correct ESM resolution. Import types from 'chart.js' (e.g., ChartType, ChartOptions, ChartDataset).
6. CSS Container Dimensions
responsive: true and maintainAspectRatio: false require the canvas container to have an explicit width/height (or flex/grid sizing). A zero-height container yields a zero-height chart — this is unrelated to modular imports but frequently misdiagnosed as a registration issue.
When This Approach Doesn't Apply
- Script-tag-only environments (no bundler) — use the UMD build and accept the full size.
- Legacy Chart.js 3 codebases using
'chart.js/auto'— migration requires explicit registration for every component. - Plugins that haven't been updated for v4 modular architecture — check the plugin's repository for a v4-compatible release before adopting.
Quick Verification Checklist
- Production build completes without warnings about side effects.
- Bundle analyzer shows only imported components.
- Runtime renders target chart type without controller errors.
- Decimation (or other plugins) functions on large datasets.
tsc --noEmitpasses with zero type errors.
If all five pass, your modular import setup is working as intended.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.