Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/quick-charts-mount.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/charts': patch
'@tanstack/react-charts': patch
---

Reduce chart mounting work by creating default SVG focus indicators only when needed and avoiding duplicate React client rendering. Speed up SVG serialization and categorical domain inference while preserving server rendering, hydration, chart appearance, and interactions.
116 changes: 116 additions & 0 deletions API-FRICTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@ Each entry records:

- Status: resolved
- Severity: high
- Owner: API
- Observed in: TanStack Charts sandbox migration
- Friction: `ChartSurface` creates a temporary runtime and prepares the dynamic
definition for initial markup. The mounted DOM host creates another runtime
Expand All @@ -581,6 +582,32 @@ Each entry records:
application reactivity owns transformed data and asynchronous cleanup.
- Verification: React and Octane dynamic mounts, hydration, and SSR retain
complete initial markup without a preparation lifecycle.
- 2026-09-08 performance follow-up: the 10,000-cell heatmap investigation
confirms that application data preparation remains outside the runtime, but
React still builds and serializes initial SVG markup, then builds the scene
again and serializes/reconciles SVG during its mount effect. A wrapped public
`renderSvg` callback runs twice on each production React mount. With default
focus rings disabled in both diagnostic cases, React SVG takes 74.4 ms median
to the Element Timing paint timestamp, while mounting the same definition
through the direct SVG host takes 37.5 ms. Both retain 10,000 visible cells.
- Follow-up resolution: React client-only mounts now build the chart once in
the existing layout effect, using DOM text metrics. A hydration snapshot
retains complete initial markup for server rendering and hydration. No
chart options or application changes are required.
- Verification: client SVG and canvas builders run once, SVG serialization
runs once, and the mounted chart is available to parent layout effects.
Strict Mode hydration retains SVG and mark identity without recoverable
errors. Combined with F-225, the same heatmap takes 39.4 ms for SVG and
24.5 ms for canvas, down from 130.1 and 35.9 ms respectively.

- Construction/layout follow-up: React ran a second adapter update with the
same options immediately after mounting. That update refreshed computed
typography after inserting SVG. The adapter now mounts or updates once per
commit and retains explicit unmount cleanup. Later updates still refresh
fonts, and Strict Mode hydration and chart identity remain covered.
- Verification: a regression checks that initial mounting does not read chart
surface styles after SVG insertion and that a later font change still
triggers rendering. All 903 core and React tests and TypeScript pass.

### F-012 — Render callbacks omit diagnostic metrics

Expand Down Expand Up @@ -6920,6 +6947,88 @@ Each entry records:
focus layer. DOM, adapter, renderer-neutral, React SSR/hydration, and React
Native tests cover the disabled focus contract. The catalog test and packed
consumer gate server-render all 110 catalog components.
- 2026-09-08 performance follow-up: a 100 by 100 cell matrix with tooltip and
keyboard disabled still emits 10,000 hidden default-focus circles, as its
pointer focus remains enabled. Production React SVG takes 130.1 ms median
to the Element Timing paint timestamp. Setting only `focusRing: false`
reduces that to 74.4 ms and SVG markup from 2,837,699 to 1,138,821 characters.
All 10,000 visible cells remain. Canvas changes from 35.9 to 33.3 ms.
- Follow-up resolution: the SVG surface serializes only active default focus
geometry. The scene retains all point geometry, authored focus layers keep
their behavior, and animated updates materialize previous geometry before
interpolation. The `focus: false` contract is unchanged.
- Verification: all 10,000 cells remain. Native Chromium pointer and keyboard
interactions match HEAD, with byte-identical screenshots and stable base
cell identity. Tests cover clipping, paint resources, custom serialization,
blur, animation interpolation, and cleanup after a serializer throws.
Mounting without axes takes 39.4 ms for SVG and 24.5 ms for canvas;
with axes it takes 43.6 and 26.9 ms, down from 171.2 and 37.6 ms.
Initial SVG contains 10,007 elements instead of 20,007.
- Bundle review: shared renderer and adapter changes add 451 gzip bytes to
the representative React line consumer. Locked bundle baselines and only
exceeded ceilings were updated by their measured deltas; dependency
isolation checks pass. Competitor bundle measurements are unchanged.
- Test status: the final focused suite passes all 91 tests and TypeScript
passes. The full workspace root suite passes 1,782 tests; two existing
catalog assertions still expect 110 cases and omit `120-sales-funnel`.
Neither those assertions nor the catalog inputs changed in this task.
- Second performance pass: CPU profiling found allocations in SVG
serialization. SVG escaping now uses one pass, and shared clip, formatting,
and focus-layer routines remove duplicate implementations. Group traversal
avoids temporary arrays, paint serialization avoids per-node closures, and
scenes without gradients skip gradient lookup. A key-lookup experiment was
discarded because scene timings did not show a consistent benefit. Scene
output, identity rules, and enabled interactions are unchanged.
- Second-pass verification: all 901 core and React tests and TypeScript pass.
Seven native Chromium screenshots match HEAD byte for byte, including axes,
SVG, canvas, and pointer/keyboard focus. The representative React line
consumer adds 334 gzip bytes over HEAD, down from 451 bytes in the first
pass. Reviewed bundle ceilings track actual entry-point deltas and retain
dependency isolation. Interleaved before/after samples and the CPU profile
are in `.benchmark-output/heatmap-investigation/round2/`.
- Final second-pass timings: 30 interleaved measured mounts per version after
10 warmups give SVG paint medians of 40.3 to 37.5 ms without axes and 44.2
to 39.6 ms with axes. Isolated serialization drops from 4.3 to 1.9 ms;
scene construction remains effectively unchanged at 9.8 versus 9.9 ms.
These compare the first optimization with the retained second-pass code,
rather than comparing separate historical timing runs.
- Third pass: scene construction skips the point-translation lookup when no
viewport is active. Categorical domain inference uses primitive identities
and a separate Date timestamp set, avoiding filtered copies and temporary
key strings. Tests retain insertion order, distinguish Date/number/string
inputs, skip invalid values, and preserve empty factory domains. Seven
native Chromium screenshots remain byte-identical to HEAD.
- Third-pass bundle review: the representative React line consumer adds
372 gzip bytes over HEAD, 38 bytes above the previous pass and still below
the first pass's 451 bytes. Renderer dependency isolation checks pass.
Raw before/after samples and style-read evidence are retained in
`.benchmark-output/heatmap-investigation/round3/`.
- Final third-pass timings: interleaved medians give scene construction at
9.8 to 8.6 ms and synchronous SVG mount at 33.9 to 24.9 ms. Paint drops
from 35.4 to 33.1 ms for SVG and 23.3 to 21.5 ms for canvas without axes;
with axes it drops from 38.0 to 35.9 ms and 26.1 to 23.1 ms respectively.
The lifecycle-only probe shows why synchronous mount and paint must remain
separate measurements: avoiding a forced style read moves browser work
out of the commit but does not by itself remove that work before paint.
- Release port: the historical measurements above used the 0.9.0 worktree.
The release is based on 0.16.1. It keeps the newer mapped focus-coordinate
registration, so the no-viewport point-map shortcut is not included.
All 973 core and React tests pass on that base, and seven native Chromium
screenshots match the current baseline byte for byte. The React line
consumer adds 365 gzip bytes, with dependency isolation preserved.
- Release measurement against 0.16.1: 30 interleaved measured mounts after 10
warmups per version give SVG paint medians of 133.4 to 35.9 ms without axes
and 164.3 to 37.9 ms with axes. Canvas gives 38.8 to 23.6 ms without axes
and 42.8 to 25.8 ms with axes. All cases render the same 10,000 cells at
500 by 300 pixels on the M5 Pro in production React and Chromium. These
measure mounting through paint, not network loading or dashboard totals.
- Measurement: Apple M5 Pro, Chromium 151.0.7922.34, production React profiling
build, 500 by 300 pixels, 10 warmups and 20 measured mounts, median without
outlier removal, no axes, animation, or progressive rendering. Workspace
revision `1b1df994b5c224dda00ef90664c5a44589b53cd9`. This reconstructs the
workload and does not claim to reproduce Colm Tuite's unpublished harness.
Local fixture, runner, screenshots, and raw samples are in
`.benchmark-output/heatmap-investigation/`.

### F-226 — Worker runtimes rejected bundled CSV parsing

Expand Down Expand Up @@ -7761,6 +7870,13 @@ Each entry records:
dependencies. All 12 `0.9.0` release artifacts pass with the unified fixture
installing from its isolated store.

- Performance release tooling: the isolated unified consumer had no
`packageManager`, so Corepack selected pnpm 12.3.4 instead of the repository's
pinned pnpm 11.15.1 and rejected the install flags. The fixture now inherits
the root package-manager pin, preserving its offline dependency check.
Verification: all seven framework adapter gates and the unified packed
artifact gate pass with the repository-pinned package manager.

### F-258 — Tooltip chrome required specificity overrides

- Status: resolved
Expand Down
40 changes: 20 additions & 20 deletions benchmarks/bundle-size/universal-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,44 @@
"policy": "Exact minified and gzip output for entries that optional features must not affect. Review every change before updating.",
"bundles": {
"D3-scale line scene": {
"bytes": 50813,
"gzip": 19070
"bytes": 50942,
"gzip": 19130
},
"D3-scale line + static SVG": {
"bytes": 55544,
"gzip": 20776
"bytes": 55598,
"gzip": 20866
},
"Representative marks": {
"bytes": 75617,
"gzip": 27582
"bytes": 75674,
"gzip": 27649
},
"TanStack DOM host": {
"bytes": 74947,
"gzip": 26053
"bytes": 75211,
"gzip": 26339
},
"React adapter": {
"bytes": 77130,
"gzip": 26831
"bytes": 77508,
"gzip": 27190
},
"React line consumer": {
"bytes": 100766,
"gzip": 36222
"bytes": 101143,
"gzip": 36587
},
"Compact-scale line scene": {
"bytes": 33265,
"gzip": 11935
"bytes": 33395,
"gzip": 11979
},
"React compact-scale line consumer": {
"bytes": 83266,
"gzip": 29144
"bytes": 83644,
"gzip": 29509
},
"Custom-scale line scene": {
"bytes": 31447,
"gzip": 11193
"bytes": 31577,
"gzip": 11249
},
"D3 linear-scale line scene": {
"bytes": 50745,
"gzip": 19032
"bytes": 50874,
"gzip": 19092
}
}
}
128 changes: 64 additions & 64 deletions benchmarks/comparison/bundle-baseline.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"schemaVersion": 4,
"generatedAt": "2026-09-08T22:11:29.376Z",
"generatedAt": "2026-09-08T22:47:47.702Z",
"packageVersions": {
"tanstack": "0.16.0",
"tanstack": "0.16.1",
"chartjs": "4.5.1",
"echarts": "6.1.0",
"recharts": "3.10.1",
Expand All @@ -11,8 +11,8 @@
"sources": {
"tanstack": {
"kind": "workspace",
"revision": "3df87d71f0305e5a450c10b66940f76f3e14259a",
"inputDigest": "sha256:40278798ed7b78aa3777062fc6ff810a50244823da5e49d9700eeef8b536994f"
"revision": "2310d3eb45088cee610565ad6f9341b72728f1f4",
"inputDigest": "sha256:2d32ae988429cf2b27eaca8a6621aba7d7deeb28be6ad79d7770c406690a6fbf"
},
"chartjs": {
"kind": "package",
Expand Down Expand Up @@ -45,88 +45,88 @@
},
"bundles": {
"tanstack-line-basic": {
"minifiedBytes": 110996,
"gzipBytes": 40024,
"brotliBytes": 35426,
"incrementalGzipBytes": 40024,
"incrementalBrotliBytes": 35426
"minifiedBytes": 111259,
"gzipBytes": 40353,
"brotliBytes": 35711,
"incrementalGzipBytes": 40353,
"incrementalBrotliBytes": 35711
},
"tanstack-line-interactive": {
"minifiedBytes": 116437,
"gzipBytes": 41782,
"brotliBytes": 36848,
"incrementalGzipBytes": 41782,
"incrementalBrotliBytes": 36848
"minifiedBytes": 116700,
"gzipBytes": 42088,
"brotliBytes": 37108,
"incrementalGzipBytes": 42088,
"incrementalBrotliBytes": 37108
},
"tanstack-line-advanced": {
"minifiedBytes": 123627,
"gzipBytes": 44128,
"brotliBytes": 38892,
"incrementalGzipBytes": 44128,
"incrementalBrotliBytes": 38892
"minifiedBytes": 123890,
"gzipBytes": 44444,
"brotliBytes": 39135,
"incrementalGzipBytes": 44444,
"incrementalBrotliBytes": 39135
},
"tanstack-bar-basic": {
"minifiedBytes": 119790,
"gzipBytes": 43347,
"brotliBytes": 38287,
"incrementalGzipBytes": 43347,
"incrementalBrotliBytes": 38287
"minifiedBytes": 120053,
"gzipBytes": 43647,
"brotliBytes": 38517,
"incrementalGzipBytes": 43647,
"incrementalBrotliBytes": 38517
},
"tanstack-bar-interactive": {
"minifiedBytes": 124086,
"gzipBytes": 44720,
"brotliBytes": 39309,
"incrementalGzipBytes": 44720,
"incrementalBrotliBytes": 39309
"minifiedBytes": 124349,
"gzipBytes": 45008,
"brotliBytes": 39660,
"incrementalGzipBytes": 45008,
"incrementalBrotliBytes": 39660
},
"tanstack-bar-advanced": {
"minifiedBytes": 124425,
"gzipBytes": 44868,
"brotliBytes": 39495,
"incrementalGzipBytes": 44868,
"incrementalBrotliBytes": 39495
"minifiedBytes": 124688,
"gzipBytes": 45146,
"brotliBytes": 39739,
"incrementalGzipBytes": 45146,
"incrementalBrotliBytes": 39739
},
"tanstack-area-basic": {
"minifiedBytes": 116205,
"gzipBytes": 42004,
"brotliBytes": 37190,
"incrementalGzipBytes": 42004,
"incrementalBrotliBytes": 37190
"minifiedBytes": 116468,
"gzipBytes": 42287,
"brotliBytes": 37422,
"incrementalGzipBytes": 42287,
"incrementalBrotliBytes": 37422
},
"tanstack-area-interactive": {
"minifiedBytes": 121650,
"gzipBytes": 43727,
"brotliBytes": 38578,
"incrementalGzipBytes": 43727,
"incrementalBrotliBytes": 38578
"minifiedBytes": 121913,
"gzipBytes": 44033,
"brotliBytes": 38900,
"incrementalGzipBytes": 44033,
"incrementalBrotliBytes": 38900
},
"tanstack-area-advanced": {
"minifiedBytes": 129022,
"gzipBytes": 46195,
"brotliBytes": 40688,
"incrementalGzipBytes": 46195,
"incrementalBrotliBytes": 40688
"minifiedBytes": 129285,
"gzipBytes": 46478,
"brotliBytes": 40997,
"incrementalGzipBytes": 46478,
"incrementalBrotliBytes": 40997
},
"tanstack-scatter-basic": {
"minifiedBytes": 112025,
"gzipBytes": 40405,
"brotliBytes": 35796,
"incrementalGzipBytes": 40405,
"incrementalBrotliBytes": 35796
"minifiedBytes": 112288,
"gzipBytes": 40704,
"brotliBytes": 36037,
"incrementalGzipBytes": 40704,
"incrementalBrotliBytes": 36037
},
"tanstack-scatter-interactive": {
"minifiedBytes": 117466,
"gzipBytes": 42150,
"brotliBytes": 37148,
"incrementalGzipBytes": 42150,
"incrementalBrotliBytes": 37148
"minifiedBytes": 117729,
"gzipBytes": 42455,
"brotliBytes": 37423,
"incrementalGzipBytes": 42455,
"incrementalBrotliBytes": 37423
},
"tanstack-scatter-advanced": {
"minifiedBytes": 117482,
"gzipBytes": 42155,
"brotliBytes": 37106,
"incrementalGzipBytes": 42155,
"incrementalBrotliBytes": 37106
"minifiedBytes": 117745,
"gzipBytes": 42460,
"brotliBytes": 37392,
"incrementalGzipBytes": 42460,
"incrementalBrotliBytes": 37392
},
"chartjs-line-basic": {
"minifiedBytes": 137909,
Expand Down
Loading