How you import Chart.js decides how many kilobytes the visitor downloads
In a real production build, what is the difference between `chart.js/auto` and a selective `Chart.register()` — in kilobytes?
Finding
Selective registration saves 9.7 kB gzip over `chart.js/auto` (67.8 → 58.1 kB, 14.3%). The larger drop is not in the library core but in leaving unused controllers out: a page that registers only the bar chart falls to 46.0 kB — two thirds of auto.
- Selective register
- 58.1 kB -14.3%
- chart.js/auto
- 67.8 kB
- Bar only
- 46.0 kB -33.9%
- Island cost of the page
- 65.3 kB
Method
Four separate `npx astro build` runs against the same repository, the same chart component and the same content file; only the import/register lines of `chart-render.ts` changed between runs. What is measured is the `gzip -9` size of the `dist/_astro/chart-render.*.js` chunk Rolldown emits. The build is deterministic, so one run suffices: identical input produced identical bytes, and each of the four strategies was run twice to confirm.
- Measured on
- Published
measured 3 days ago
Environment
- Astro
- 7.1.3
- Chart.js
- 4.5.1
- Bundler
- Rolldown (Astro 7 default)
- Node
- 24.18.0
- OS
- macOS (Darwin 25.6)
- Compression
- gzip -9
Technologies
To reproduce
npx astro build && gzip -9 -c dist/_astro/chart-render.*.js | wc -c For years this site had one rule: no required client-side JavaScript. I broke it deliberately when opening the Research section — a multi-series measurement is not readable in a chart without tooltips. But the difference between “broke it” and “cared about it” shows up in whether you measure the bytes you ship.
Chart.js documents two ways. The short one:
import Chart from 'chart.js/auto';
The long one:
import { Chart, BarController, BarElement, CategoryScale, LinearScale } from 'chart.js';
Chart.register(BarController, BarElement, CategoryScale, LinearScale);
Both work. My question was whether the difference is a measurable number in a real build, or a micro-optimisation.
What I measured
I did not set up a synthetic bundler — what I wanted to know is not “how small
can Chart.js get” but how much this repository ships. So all four runs went
through the site’s own astro build: the same MDX entry, the same chart
component, the same Preact island. The only thing that changed between runs was
the first ten lines of src/components/research/chart-render.ts.
Gzip size of the Chart.js chunk, by import strategy
All four draw the same chart. The difference is only which controllers and plugins enter the bundle.
kB lower is better Source: astro build output, dist/_astro/chart-render.*.js, gzip -9
Data table
| Series | chart.js/auto | bar+line+tooltip+filler | bar+line, no plugins | bar only |
|---|---|---|---|---|
| gzip | 67.8 kB | 58.1 kB | 51.2 kB | 46 kB |
The chart is drawn in the browser; the table below carries the same data.
With raw (uncompressed) sizes alongside:
| Strategy | Raw | gzip | vs. auto |
|---|---|---|---|
| chart.js/auto | 204,965 B | 69,466 B | — |
| bar + line + Tooltip + Filler used on this site | 172,313 B | 59,538 B | −9,928 B (14.3%) |
| bar + line, no plugins | 149,793 B | 52,378 B | −17,088 B (24.6%) |
| bar only | 134,868 B | 47,114 B | −22,352 B (32.2%) |
What the number says
Selective registration saved 9.7 kB gzip over auto. That is the real
figure behind the documentation’s “tree-shaking is supported” — and on its own
it is modest: 14.3%.
The actual information is in rows two and three. Dropping the Tooltip and
Filler plugins takes another 7.2 kB; a page that never draws a line chart
saves 5.3 kB more. In other words, most of the cost is not in the library
core but in chart types you never draw. Which is exactly what auto does: it
registers all of them.
What the whole page pays
The Chart.js chunk alone is not meaningful; when the island hydrates, the visitor downloads:
| Chunk | gzip | What |
|---|---|---|
| chart-render | 58.1 kB | Chart.js + theme bridge |
| preact | 4.9 kB | runtime core |
| hooks | 0.8 kB | useRef / useEffect |
| client | 0.8 kB | @astrojs/preact client bridge |
| ChartIsland | 0.6 kB | the component itself |
| Total | 65.3 kB |
The framework side is under 10% of the total (6.3 kB). That does not mean
choosing Preact over React would not have mattered here — quite the opposite:
react-dom alone approaches the size of Chart.js. But that is a separate
measurement and a separate record.
What I did
chart-render.ts opens with these lines and never touches the auto entry:
import {
BarController, BarElement, LineController, LineElement, PointElement,
CategoryScale, LinearScale, Tooltip, Filler,
} from 'chart.js';
Chart.register(
BarController, BarElement, LineController, LineElement, PointElement,
CategoryScale, LinearScale, Tooltip, Filler,
);
Legend is not on the list: the legend is rendered as HTML on the server. That
way it is styled with the site’s own tokens and it stays in place even if no
JavaScript runs at all — the same reasoning as the “Data table” disclosure
below every chart on this page.
Related posts
TypeScript's new compiler and why build speed matters
TypeScript's compiler rewritten in Go, and why a fast feedback loop is so valuable for developers.
Fast from scratch, slow in a live system: the real cost of brownfield in the age of agents
Work an agent finishes in days takes a week inside a system that has been running for years — because there the cost isn't producing code, it's finding the rules and proving them.