Skip to content
microcharts

Performance

Where the size and speed numbers come from: 106 chart types at ~2–7 kB interactive · ~1–4 kB static, a static server render of about 0.03 ms each, and the commands that regenerate every figure.

Every figure on this page comes from a command you can run yourself. Each one is measured against the shipped dist, re-checked in CI, and regenerated on every release.

106
chart types
0.03 ms
median / chart
146.2/ms
fastest
0
runtime deps

Size

Each chart imports 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, so a chart that grows past its budget fails the build.

interactive gzip · distributionstatic 1.084.25
2.25.26.9 kB
Count · catalog
105charts
Size range
2.26.9kB
median 5.24 · static median 2.76 · hover
Interactive median 5.24 kB across 105 entries. On the static twin, 10 of 106 charts ship under 2 kB; 74 under 3 kB. Above the 3 kB static mark — Sparkline (4.25 kB), Slope (4.06 kB), DualSparkline (3.73 kB), WinProbWorm (3.67 kB), ChangePoint (3.66 kB), ForecastCone (3.59 kB), BurnChart (3.48 kB), StationGlyph (3.48 kB), RetentionCurve (3.44 kB), TapeGauge (3.42 kB), PercentileLadder (3.41 kB), StackedArea (3.38 kB), SpreadBand (3.38 kB), ControlStrip (3.37 kB), QueueDepth (3.37 kB), SparkBar (3.36 kB), NetFlow (3.33 kB), Dumbbell (3.32 kB), PolarClock (3.32 kB), EnsembleGhosts (3.3 kB), ShiftHistogram (3.28 kB), ABStrips (3.25 kB), LikertStrip (3.19 kB), Constellation (3.16 kB), MiniBar (3.15 kB), Waterfall (3.11 kB), CyclePlot (3.11 kB), EventTimeline (3.09 kB), BenchmarkStrip (3.07 kB), GradedBand (3.07 kB), PercentileTrace (3.04 kB), VolumeProfile (3.02 kB).

The /interactive subpath lands between 2.18 kB and 6.94 kB gzip, with a median of 5.24 kB. That buys hover, roving keyboard focus, select-to-pin, touch scrub, and live announcements. Its static twin is pure SVG with zero client JavaScript, and lands between 1.08 kB and 4.25 kB, with a median of 2.76 kB. A page pays for interactivity only where it 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 chartsChartStaticInteractive
smallestTokenConfidence1.08 kB2.35 kB
medianBiasStrip2.76 kB5.29 kB
largestSparkline4.25 kB6.94 kB

Thirty-two charts sit above the 3 kB static reference line, none of them by more than 1.25 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, PercentileTrace, ShiftHistogram, StackedArea, ABStrips, PolarClock, Constellation, Dumbbell, CyclePlot, Waterfall, TapeGauge, MiniBar, BenchmarkStrip, EventTimeline, VolumeProfile, GradedBand and LikertStrip; the last two crossed the line when their percent labels moved onto a real Intl formatter. The annotation hosts carry a small shared walker that resolves Threshold, Marker, and the rest against the chart's scale, and you pay for it only on those charts.

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 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. The split trades bytes and changes no behavior. 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 and zero client JavaScript reaches the browser. Throughput, measured by pnpm bench rendering N sparklines to an SVG string:

ChartsTimePer chartPayload
1004.1 ms0.041 ms~572 B
5005.5 ms0.011 ms~572 B
100010.7 ms0.011 ms~572 B

A table of 500 sparklines becomes HTML in under six milliseconds, streams as part of the document, and hydrates nothing. 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 close to nothing.

The summary kernel is just as cheap: describeSeries runs at roughly 840,000 calls per second on a 24-point series, measured by pnpm bench. It writes the accessible sentence for every chart, so accessibility is not what slows a render down.

Why it's fast

  • 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. There is no transitive tree to resolve, to audit, or to carry in a lockfile.
  • Integer geometry, crisp marks. Coordinates are rounded at generation and rectilinear marks use shape-rendering: crispEdges, so the browser does less work rasterizing 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, which is what keeps them RSC-safe, so they can't memoize internally: a parent re-render re-runs each chart's geometry. That is 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);

memo only helps when the props are 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 memo.

The charts don't ship wrapped in memo for that reason: it would tax the common case to speed up a narrow one, and the call is yours to make.

Caveats

  • The bench runs in one process, and that used to skew it. Measuring 106 components in sequence left 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 bench now 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 bench exits non-zero and names it.
  • Interactive entries do ship JavaScript. The /interactive subpath adds a client component, a small set of pointer, keyboard, and touch listeners on the wrapper, and 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 is small but it isn't zero, so reach for the static entry unless a chart needs hover, keyboard, selection, or live announcements.
  • These numbers come from one machine and one Node. They measure relative cost and reproduce on your hardware; treat them as a floor to reproduce rather than 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.json

CI runs --check on both synced files, so a stale number fails the build the same way a failing test does.