Skip to content
Muhammet Şafak
tr

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.

High confidence Repeated runs, controlled environment, raw data published.
Measured on

measured 4 days ago

Published
Updated

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

Chart.js Astro Rolldown Preact

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
astro build output, dist/_astro/chart-render.*.js, gzip -9
Series chart.js/autobar+line+tooltip+fillerbar+line, no pluginsbar only
gzip 67.8 kB58.1 kB51.2 kB46 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%)
Raw and gzip move in the same direction but not at the same rate: compression was already squeezing part of the code that got deleted.

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
This is the island cost of a research page with one chart. The `signals.module` chunk is emitted by the build but the page never requests it — @astrojs/preact loads it only when `data-preact-signals` is present.

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

Share:

Updated:

Other records

All records

Laravel's preload curve: 123 files buy eight times what the last 1,912 do

How far can a curated preload take Laravel, and what does each slice cost in start-up time?

Finding

The curve is not proportional to volume. The first 1,592 files — Laravel's own framework — buy 30 ms and add 1.2 seconds to start-up. The next 1,094 Symfony files buy 9.5 ms for free. The **123 files** after that (psr, carbon) buy 15.7 ms, more than the 1,094 before them. And the last 1,912 buy 1.8 ms while adding another 1.2 seconds. So the blanket preload the earlier record measured as a ceiling is the worst point on the curve that is not the origin: stopping at 2,809 files gives 12.77 ms for 1,514 ms of start-up, while 4,721 files ask 2,691 ms to reach 10.96 ms.

measured yesterday

High confidence

A partial index makes a queue table forty-one times smaller — for as long as the planner picks it

On a Postgres queue table with millions of dead rows, what does a partial index buy, and when does the planner refuse to use it?

Finding

At 10 million dead rows a partial index sustains 11,537 claims per second where the same table without one manages 7. Against a composite index the throughput difference is small (6.9%) but the size difference is not: 7.6 MB against 310.4 MB, and the partial one does not grow with the table because it indexes only the 5,000 live rows. None of that is the real finding. The moment the planner switches a prepared statement to a generic plan the partial index stops being used at all — 11,752 tps becomes 7, and 0.68 ms becomes 1.1 seconds. A factor of 1,673. The composite index is untouched under the same conditions.

measured yesterday

High confidence

The partial index grew three hundred and eighty times in fifteen minutes — and autovacuum never ran

Under sustained churn, does a partial index stay small on a queue table, and do the default autovacuum settings keep up with it?

Finding

With the live set holding steady at five thousand rows the partial index went from 0.1 MB to 38.2 MB — three hundred and eighty times. Its smallness comes from the live set, its bloat rate comes from throughput, and nothing connects the two. The composite index bloated less in proportion (42%) and more in absolute terms (+126 MB), and while bloating it stopped fitting in memory: its latency went from 0.52 ms to 61 seconds and its backlog climbed to 126,000. Fifteen minutes produced 1.75 million dead rows and autovacuum **did not run once** — the default threshold scales with the whole table (50 + 0.2 × 10 million ≈ 2 million) while the churn happens in a tiny subset.

measured yesterday

High confidence

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind