Skip to content

perf(table-core): shape-stable row/cell/column/header instances - #6569

Draft
KevinVandy wants to merge 2 commits into
mainfrom
perf/shape-stable-instances
Draft

perf(table-core): shape-stable row/cell/column/header instances#6569
KevinVandy wants to merge 2 commits into
mainfrom
perf/shape-stable-instances

Conversation

@KevinVandy

Copy link
Copy Markdown
Member

Summary

Keeps one V8 hidden class per instance kind (row, cell, column, header) for the whole instance lifetime, so property access across rows/cells stays monomorphic instead of forking into megamorphic ICs. This is the fix for the Object.assign-on-rows / lazy-memo-property pattern flagged in the Discord perf thread.

  • Memo storage declared up front: memoized prototype APIs keep memo state in a pre-declared _memos holder (dedicated memoSlots for the render-hot ones: cell.getContext, row.getAllCells, row.getVisibleCells, ...) instead of installing _memo_<fnKey> own properties on first call, which was the main shape-forker.
  • Per-table constructor functions for rows and cells (new TableRow(...) instead of Object.create(proto) + assigns): every field lives in-object, no out-of-line property array that reallocates as fields are added. This flipped construction from a regression into the largest win and is most of the memory improvement.
  • Warmup instance per table (warmInstanceShape): rewrites each declared field once at prototype creation so V8 never assumes field constness and deopts on the first real write (grouped row fields, memo slots, pinned position).
  • Every post-construction own-property write declared: grouping row fields (the Object.assign(row, {..., getValue: closure}) becomes value writes + a fixed-arity prototype getValue override; _groupedRows kept beside leafRows to preserve exact tree-data aggregation), _cellsCache, originalSubRows, row/cell pinned position; sorted clones rebuilt via constructRow; worker rebuild seeds caches instead of an own closure.
  • Lazy cache maps: _uniqueValuesCache / _groupingValuesCache allocate on first use; columnFilters(Meta) start as a shared frozen empty map (public types unchanged, zero per-row allocation).
  • New tests/unit/shapeStability.test.ts: own-key-order identity across leaf/group/pinned/cloned rows and cells, before and after every API call (the natives-free proxy for map identity), plus grouped getValue goldens.

Results (vs 9.1.2, 100k rows unless noted)

Measurement 9.1.2 this PR
%HaveSameMap shape gate across rows/cells/columns/headers fails (rows fork ~10 shapes) passes
Browser grouped stage (bench:row-model grouping:sum) 39.7 ms 30.6 ms (-23%)
Browser heap, paginated 100k / 1M rows 39.5 MB / 379 MB 31.5 MB / 299 MB (-20%)
Node construction, rich feature set ~83 ms ~54 ms (-35%)
Node steady-state render-access sweep ~244 ms ~245 ms (parity)
e18e/deopt wrong-map deopts on hot row/cell APIs present eliminated
size-limit 24.93 KB 25.17 KB

Full monorepo CI and all 388 example e2e projects pass. Benchmark harness (shape gate, access sweep, A/B recipe) lives in the benchmark-examples repo.

Notes for review

  • Row_ColumnGrouping gained defaulted generics (<TFeatures, TData>) to type leafRows/_groupedRows.
  • position?: 'top' | 'bottom' is now typed on Row_RowPinning.
  • Follow-ups (not in this PR): global per-feature-set prototype cache (cross-table map polymorphism is the last remaining wrong-map source), and the write-epoch memo fast path stacked as the next PR in this chain.

🤖 Generated with Claude Code

Keep one hidden class per instance kind for the whole instance lifetime so
V8 property access stays monomorphic:

- memoized prototype APIs store memo state in a pre-declared `_memos`
  holder (plus dedicated `memoSlot`s for render-hot APIs) instead of
  installing `_memo_<fnKey>` own properties on first call
- rows and cells allocate through per-table constructor functions so every
  field lives in-object; a discarded warmup instance per table pre-marks
  declared fields mutable to avoid field-constness deopt waves
- all post-construction own-property additions declared up front: grouping
  row fields (the `Object.assign` + per-row `getValue` closure becomes a
  fixed-arity prototype override), `_cellsCache`, `originalSubRows`, pinned
  `position` marks; sorted clones rebuilt via `constructRow`
- eager per-row cache maps made lazy (`_uniqueValuesCache`,
  `_groupingValuesCache`); filter maps start as a shared frozen empty map

Browser (100k rows, vs 9.1.2): grouped stage -23%, heap -20%; Node
construction -21..-35%; steady-state access at parity; %HaveSameMap shape
gate passes (fails on 9.1.2). Adds tests/unit/shapeStability.test.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: eebcdbff-acb8-49b8-9ec0-cf04afd5dd38

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Aug 21, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit b164a93

Command Status Duration Result
nx affected --targets=test:eslint,test:sherif,t... ✅ Succeeded 5m 45s View ↗
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 29s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-22 15:20:27 UTC

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

1 package(s) bumped directly, 0 bumped as dependents.

🟩 Patch bumps

Package Version Reason
@tanstack/angular-table 9.2.1 → 9.2.2 Changeset

@pkg-pr-new

pkg-pr-new Bot commented Aug 21, 2026

Copy link
Copy Markdown
More templates

@tanstack/alpine-table

npm i https://pkg.pr.new/@tanstack/alpine-table@6569

@tanstack/angular-table

npm i https://pkg.pr.new/@tanstack/angular-table@6569

@tanstack/angular-table-devtools

npm i https://pkg.pr.new/@tanstack/angular-table-devtools@6569

@tanstack/ember-table

npm i https://pkg.pr.new/@tanstack/ember-table@6569

@tanstack/lit-table

npm i https://pkg.pr.new/@tanstack/lit-table@6569

@tanstack/match-sorter-utils

npm i https://pkg.pr.new/@tanstack/match-sorter-utils@6569

@tanstack/octane-table

npm i https://pkg.pr.new/@tanstack/octane-table@6569

@tanstack/preact-table

npm i https://pkg.pr.new/@tanstack/preact-table@6569

@tanstack/preact-table-devtools

npm i https://pkg.pr.new/@tanstack/preact-table-devtools@6569

@tanstack/react-table

npm i https://pkg.pr.new/@tanstack/react-table@6569

@tanstack/react-table-devtools

npm i https://pkg.pr.new/@tanstack/react-table-devtools@6569

@tanstack/solid-table

npm i https://pkg.pr.new/@tanstack/solid-table@6569

@tanstack/solid-table-devtools

npm i https://pkg.pr.new/@tanstack/solid-table-devtools@6569

@tanstack/svelte-table

npm i https://pkg.pr.new/@tanstack/svelte-table@6569

@tanstack/table-core

npm i https://pkg.pr.new/@tanstack/table-core@6569

@tanstack/table-devtools

npm i https://pkg.pr.new/@tanstack/table-devtools@6569

@tanstack/vue-table

npm i https://pkg.pr.new/@tanstack/vue-table@6569

@tanstack/vue-table-devtools

npm i https://pkg.pr.new/@tanstack/vue-table-devtools@6569

commit: b164a93

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant