Skip to content
Muhammet Şafak
tr
Frontend performance

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 3 days ago

Published

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:

Other records

All records
Tooling comparison Measurement

The fixed cost of installing a PHP framework: disk size tells you nothing

How many megabytes on disk, how many files per request and how many milliseconds on the first request do seven PHP frameworks cost — and which of those numbers actually predicts throughput under load?

Finding

Disk size predicts nothing: Yii2 has the largest vendor tree at 34.1 MB and loads only 62 files per request, among the fewest in the field. Files per request predicts nothing either: CodeIgniter loads 96 files and serves 6,431 req/s, Symfony loads 224 and serves 13,067. The one number that genuinely separates them is the first request served with a cold opcache: 2.2 ms for Phalcon, 72.2 ms for Laravel — thirty-three times. That is the compile bill the first visitor pays after every deploy, and it is measured in tens of milliseconds, not kilobytes.

High confidence
Service & load Measurement

Seven PHP frameworks under identical load: the gap narrows as soon as the request does real work

On the same hardware, the same PHP build and the same seven routes, how many requests a second do Laravel, Symfony, CodeIgniter, Yii2, Phalcon, Laminas and Slim serve, and at what latency?

Finding

On an empty route the fastest is 4.4× the slowest (Slim 25,975, Laravel 5,966 req/s). As soon as the request does real work the gap closes: 3.7× for a single row from the database, 3.5× for twenty rows. Phalcon is third on an empty route and fifth once a query is involved — being a C extension buys nothing while the process waits on MySQL. And the expensive decision is not the framework: Laravel's own default `web` middleware group takes the same response from 5,858 to 2,176 req/s, so one default costs more than most of the distance between the frameworks.

Medium confidence

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind