# Performance (/docs/performance)

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.

## 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.

The `/interactive` subpath lands between **2.28 kB and 6.96 kB** gzip, with a **median of 5.3 kB**. That buys hover,
roving keyboard focus, select-to-pin (and light dismiss), touch scrub, and live announcements. Its static twin is pure
SVG with zero client JavaScript, and lands between **1.08 kB and 4.24 kB**, with a **median of 2.78 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.

Thirty-two charts sit above the 3 kB static reference line, none of them by more than 1.24 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.

Every number on this page is measured in this repo. For a reading nobody here produced, open the package on
[Bundlephobia](https://bundlephobia.com/package/@microcharts/react): it installs the published tarball, counts the
dependencies, and sizes the root entry. Its figure covers the package root rather than a per-chart subpath, so read it
as a check on the dependency count, not as the number a chart costs you.

### 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.

```ts
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:

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 square-cornered 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:

```tsx
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.

```bash
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.
