Performance
Zero dependencies, budget-gated sizes, and server rendering with no client JavaScript — 106 chart types at ~2–7 kB interactive · ~1–4 kB static, static SSR median about 0.03 ms each. Every number here is reproducible from one command.
Performance isn't a marketing line here — it's a build gate. Every figure on this page is produced by a command you can
run yourself, checked against the shipped dist in CI, and regenerated on every release so it can never drift from
reality.
Size
Each chart is imported from its own subpath, so a bundle ships only the charts it uses — importing Sparkline never
pulls in the other 105. Sizes are gzip, measured by pnpm size and gated: a chart that grows past its budget fails the
build.
The interactive catalog lands between 1.88 kB and 6.64 kB gzip, with a median of 5.03 kB — hover, roving
keyboard focus, select-to-pin, touch scrub, and live announcements from the /interactive subpath. The static twin
lands between 0.97 kB and 3.99 kB, with a median of 2.6 kB — pure SVG, zero client JavaScript. A page pays for
interactivity only where it actually uses it. About 0.5 kB of the interactive delta is the shared picker kernel, which
the per-subpath gate measures standalone in every entry; a page using several interactive charts bundles it once.
| Across all 106 charts | Chart | Static | Interactive |
|---|---|---|---|
| smallest | TokenConfidence | 0.97 kB | 1.88 kB |
| median | WindBarb | 2.60 kB | kB |
| largest | Sparkline | 3.99 kB | 6.64 kB |
Twenty-five charts sit above the 3 kB static reference line, none of them by more than 0.99 kB. Sparkline, the most
configurable primitive in the set, is the largest; most of the rest are value-series charts that host annotations
(ChangePoint, WinProbWorm, ForecastCone, DualSparkline, BurnChart, SpreadBand, RetentionCurve, ControlStrip, QueueDepth,
SparkBar, EnsembleGhosts, NetFlow, Slope), plus Station Glyph, PercentileLadder, ShiftHistogram, StackedArea, ABStrips,
PolarClock, Constellation, Dumbbell, CyclePlot, Waterfall, and TapeGauge. The annotation hosts carry a small shared
walker that resolves Threshold, Marker, and the rest against the chart's scale — you pay for it only on those
charts, and each is still smaller than a single icon font.
Splitting the stylesheet
@microcharts/react/styles.css is one artifact for the whole catalog, and importing it once is the right default — it
is small, it caches, and it never grows with the number of charts you use. If you ship exactly one or two charts and
want the rules for the rest gone, there is an opt-in split: @microcharts/react/styles/core.css carries everything
shared, and @microcharts/react/styles/<slug>.css carries the rules specific to a single chart. Only the charts with
chart-specific CSS have a file; the rest need core.css alone.
import "@microcharts/react/styles/core.css";
import "@microcharts/react/styles/sparkline.css";core.css plus the slugs you import is equivalent to the matching subset of styles.css — same rules, same @layer
membership, same cascade order — so the split is a byte trade, never a behavior change. Import one or the other, not
both.
Server rendering
Static charts are hook-free and listener-free, so a React Server Component renders them to plain SVG on the server —
zero client JavaScript reaches the browser. Throughput, measured by pnpm bench rendering N sparklines to an SVG
string:
| Charts | Time | Per chart | Payload |
|---|---|---|---|
| 100 | 1.2 ms | 0.012 ms | ~572 B |
| 500 | 5.5 ms | 0.011 ms | ~572 B |
| 1000 | 10.8 ms | 0.011 ms | ~572 B |
A table of 500 sparklines becomes HTML in under six milliseconds, streams as part of the document, and ships nothing to hydrate. Across all 106 chart types the median static SSR render is about 0.03 ms per chart — the heavier expressive and frontier instruments cost more geometry, the simple strips cost almost nothing.
The core summary kernel is just as cheap: describeSeries, which writes every chart's accessible sentence, runs at
roughly 892,000 calls per second (24-point series, measured by pnpm bench), so accessibility is never the thing
that slows a render down.
Why it's fast
The speed isn't a trick — it's the absence of one.
- No chart engine. No D3, no layout runtime, no virtual DOM diff for a static chart. Scales, paths, and stats are a few hundred lines of pure functions, and the output is a handful of SVG nodes — typically six or fewer per chart.
- Zero runtime dependencies.
dependencies: {}, enforced by CI, forever. Nothing to resolve, nothing to audit, nothing to bloat a lockfile. - Integer geometry, crisp marks. Coordinates are rounded at generation and rectilinear marks use
shape-rendering: crispEdges, so the browser does less work rasterising them. - One stylesheet, one cascade layer. Theming is CSS custom properties in a low-specificity
@layer, not inline styles recomputed per instance. - A scrub costs one render per unit crossed, not one per pointer move. Interactive entries put a single listener on the wrapper and resolve the unit with pure math, then bail out when the unit hasn't changed — so a sweep across a 24-point Sparkline re-renders at most 24 times no matter how many pointer events the browser delivers.
Re-rendering many charts
Static entries are hook-free — that's what keeps them RSC-safe — so they can't memoise internally: a parent re-render re-runs each chart's geometry. That's cheap per chart, but it adds up in a table of hundreds.
If you render many charts inside a component that re-renders for unrelated reasons, wrap them and keep data
referentially stable:
const Spark = memo(Sparkline);The catch is that memo only helps when props are actually stable — pass a fresh array literal (data={[1, 2, 3]}) or
JSX children and it misses every time, and you pay the comparison on top of the render you were going to do anyway.
Hoist the array, or skip it.
We don't ship memo on the charts themselves for exactly that reason: it would tax the common case to speed up a
specific one, and it's your call, not ours.
Honest caveats
- The bench is one process, and that used to bite. Measuring 106 components in sequence let earlier charts leave the
later ones cold, and a median over windows short enough for one GC pause produced bimodal numbers — the same chart
read 140 and 35 rows/ms on consecutive runs of identical code.
pnpm benchnow warms each component immediately before timing it and reports the fastest window, since GC and scheduler noise only ever slow a window down. Every chart clears its floor; if one stops,pnpm benchexits non-zero and names it. - Interactive entries do ship JavaScript. The
/interactivesubpath adds a client component and a small set of listeners on the wrapper — pointer, keyboard, and touch — plus the shared picker kernel that resolves them to a unit with pure math. There is still never a listener per data point, so a 30-point chart and a 365-cell grid cost the same. It's tiny, but it isn't zero — reach for the static entry unless a chart genuinely needs hover, keyboard, selection, or live announcements. - These numbers are one machine, one Node. They measure relative cost honestly and reproduce on your hardware; treat them as a floor to reproduce, not an absolute to quote.
Reproduce it
Every figure above regenerates from the repo. Nothing is hand-keyed — the docs read the measured output directly.
git clone https://github.com/ganapativs/microcharts
pnpm install
pnpm build # build dist the numbers are measured against
pnpm bench && node scripts/sync-bench.mjs # SSR throughput → bench-summary.json
node scripts/sync-sizes.mjs # gzip budgets → chart-sizes.jsonCI runs --check on both synced files, so a stale number fails the build the same way a failing test does.
Accessibility
Accessible React charts by default — every microchart is role="img" with a generated natural-language summary, keyboard-ready interactive entries, CVD-safe tokens, and forced-colors / reduced-motion support. No extra a11y work.
Troubleshooting
The predictable failures — unstyled charts, a chart invisible on a dark panel, a 'use client' error from the /interactive entry, the removed onPointFocus callback, an animate prop that does nothing, a chart that won't fill its container, and fonts that look wrong — each with its one-line fix.