fix: more typescript performance improvements#6320
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (36)
📝 WalkthroughWalkthroughThis PR adds TypeScript ChangesTypeScript Variance Annotations and Table Type System Restructuring
🎯 4 (Complex) | ⏱️ ~45 minutes Possibly Related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
View your CI Pipeline Execution ↗ for commit e4f2a3a
☁️ Nx Cloud last updated this comment at |
TanStack Table — TypeScript Type-Performance Report
Tracks TypeScript type-checking cost across the alpha branch, the beta branch as released (beta.10), and the optimized working tree (beta.11 candidate). Primary metric is
Instantiationsfromtsc --extendedDiagnostics(deterministic — confirmed identical across repeated runs). Wall times and memory are supporting evidence only.Setup
table-alpha/table/alpha@4f855fe4ebeta@925358059(+ uncommitted beta.11 work)@tanstack/table-coreversionpnpm build(tsdown) fortable-coreandreact-tablein the measured repo before every consumer benchmark (react-table, kitchen-sink) so declarations are current.Instantiationswas identical on every repeat.Benchmark commands
packages/table-corepnpm exec tsc --noEmit --extendedDiagnosticspackages/table-corepnpm exec tsc -p tests/tsconfig.declaration-emit.json --extendedDiagnosticspackages/react-tablepnpm exec tsc --noEmit --extendedDiagnosticsexamples/react/kitchen-sinkpnpm exec tsc --noEmit --extendedDiagnosticsThese mirror the repo's own
test:typesscript (tsc && tsc -p tests/tsconfig.declaration-emit.json).Results — Instantiations (primary metric)
@tanstack/table-coretsc --noEmit@tanstack/table-coredeclaration emit@tanstack/react-tabletsc --noEmitexamples/react/kitchen-sinktsc --noEmitPer-step deltas: alpha → beta.10 was −59.8% / −66.4% / −68.7% / −61.2%; beta.10 → beta.11 adds another −41.8% / −50.8% / −25.7% / −12.9% (same benchmark order as the table).
All framework adapters — Instantiations
pnpm exec tsc --noEmit --extendedDiagnosticsper adapter package (src + tests), all packages rebuilt first in each repo. beta.11 includes the explicit-type-args fix applied toangular-tableandpreact-table(sameconstructTablespread-inference hot spot as react'suseTable; lit/solid/svelte/vue already passed alias-typed variables and needed no change).@tanstack/angular-table@tanstack/lit-table@tanstack/preact-table@tanstack/react-table@tanstack/solid-table@tanstack/svelte-table@tanstack/vue-tableWithin beta, the
constructTableexplicit-type-args fix alone took angular-table 59,190 → 49,948 (−15.6%) and preact-table 49,125 → 41,558 (−15.4%).All kitchen-sink examples — Instantiations
angular/kitchen-sinklit/kitchen-sinkpreact/kitchen-sinkreact/kitchen-sinksolid/kitchen-sinkvue/kitchen-sink(vue-tsc)react/kitchen-sink-hero-uireact/kitchen-sink-mantinereact/kitchen-sink-material-uireact/kitchen-sink-react-ariareact/kitchen-sink-shadcn-basereact/kitchen-sink-shadcn-radixMeasurement caveats:
svelte/kitchen-sinkis excluded: plaintscdoes not check.sveltemarkup (it reports only ~2.2k instantiations from the loose.tsfiles in both repos);svelte-checkdoes not expose--extendedDiagnostics. The svelte adapter package row above covers the svelte surface.vue/kitchen-sinkis measured withvue-tsc(plaintscskips.vueSFCs and returns an identical, meaningless number in both repos). The UI-kit react variants carry large third-party type surfaces (MUI, Mantine, etc.), which dilutes the percentage but the absolute deltas (−145k to −154k) match the plain kitchen-sink.solid/kitchen-sinkhas 3 pre-existing app-level JSX errors, byte-identical in alpha and beta — unrelated to table types.Check times (secondary, two runs each): table-core 1.74s/1.83s → 1.07s/1.04s; declaration emit 1.58s/1.56s → 0.80s/0.87s; react-table 0.29s/0.26s → 0.21s/0.22s; kitchen-sink 0.53s/0.50s → 0.46s/0.50s.
The react-table regression and the variance fix
Intermediate state (before variance annotations): the
Table_Internalinterface conversion alone regressedreact-table's package-internal check to 136,031 (+84.6%). Root cause: with type parameters flowing into conditional types, the compiler's measured variance for the interface was unreliable, so relatingTable_Internal<A>toTable_Internal<B>across react's many fresh generic contexts fell back to member-by-member structural comparison, expanding the heavy members per pair. Adding explicitin out(invariant) variance annotations — the pattern used throughoutformandrouter— tells the checker to relate instantiations by their type arguments directly:Table_Internalalone took react-table from 136,031 to 65,891, and annotating the rest of the generic interfaces took it to 54,732 — 26% below the beta.10 baseline, and turned kitchen-sink from neutral to −12.9%. Variance annotations don't cache anything; they short-circuit relation checks by eliminating both variance measurement and the unreliable-variance structural fallback.beta.11 kept changes
Table_Internal→ broad interface (types/Table.ts). WasTable<TF,TD> & {…}— an intersection wrapping the deferredExtractFeatureMapTypesconditional, re-expanded structurally at every internal call site. Now an interface extendingOmit<Table_Table, broadened-keys>plus all four core API interfaces and all 14 stock featureTable_*interfaces, with the internal slots (_rowModels,_rowModelFns,options,initialState,store,atoms,baseAtoms) redeclared in their broad*_Allforms (TData-bearing conditionals dropped; TFeatures-only conditionals kept — feature-set count is small, data-type count is large). PublicTable<TF,TD>is untouched, so user-facing inference is unchanged.TableOptions_All's feature-map intersection de-U2I'd (types/TableOptions.ts). WasUnionToIntersection<Map[keyof Map]>(via aTableOptions_FeatureMap_Allhelper) — re-distributed 13 members per (features, data) pair. Now a direct intersection of the 13 named stock option interfaces (inlined intoTableOptions_All; the helper type was removed as perf-neutral, see experiment 6) plusTableOptions_PluginFeatureMapTypes, which falls back toUnionToIntersectiononly for plugin keys declaration-merged beyond the stock set ([Exclude<keyof Map, StockKeys>] extends [never]guard). Plugin flow-through verified against the custom-plugin example (mergesTableOptions_FeatureMapand readstable.options.onDensityChange).makeStateUpdatertakes a minimal structural param (utils.ts) —{ options: { atoms?: object }, baseAtoms: object }instead ofTable<TFeatures, any>. It only reads those two slots; any table view (public, internal, plugin) now passes without relating full table types. Accepts strictly more than before — non-breaking.in outvariance annotations on generic interfaces (166 sites acrosstable-core/src, applied toTFeatures/TDataparameters of exported interfaces). Both parameters are structurally invariant already (used in parameter and return positions throughout), so the annotations change no assignability semantics — TypeScript verifies annotations against structure and reported no contradictions. The win: the checker relates two instantiations of the same interface by comparing type arguments directly instead of measuring variance (which came back unreliable due to conditional-type usage) and falling back to structural member expansion. Same pattern as TanStackform(FieldApi,FormApi) androuter(Router,Route). This is what makes theTable_Internalinterface conversion a win on all benchmarks instead of a core-vs-react trade-off.createCoreRowModelfactory takesTable_Internal(removing anas unknown ascast);constructTablereturns via one explicit cast; twomergeOptionscall sites casttable.optionsdown toTableOptions; one unit test casts its constructed table toTable_Internal.constructTablecalls — reactuseTable, angularinjectTable, preactuseTableall builtconstructTable({ ...options, features: { <reactivity>, ...options.features } }), forcing generic inference + structural relation of the anonymous spread type againstTableOptions(react: ~740ms of traced check time).constructTable<TFeatures, TData>(...)skips inference. Saved ~12k (react), ~9.2k (angular), ~7.6k (preact). lit/solid/svelte/vue already passed alias-typed variables.Rejected / neutral experiments (with numbers)
ColumnDef_FeatureMapinto static/dynamic halves + plugin-residual extractor, so the TData-independent half caches per feature setRow_Coreinstead ofRow(skip deferred-conditional apparent type)ColumnHelper.accessorconditional signature into two overloads (key-first)Table_Internalas interface, members kept asInherited & AllintersectionsOmit-extends + broad slot members + de-U2I'd optionsin outonTable_Internalonlyin outon all generic interfaces, 166 sites (kept)in outon the publicTablealiasTableOptions_FeatureMap_AllintoTableOptions_All(its only internal user)in outonTValue extends CellDataparams (16 interfaces)TValue → unknowncovariant widening —Header<TF,TD,TValue>no longer assignable toHeader<TF,TD,unknown>(buildHeaderGroups.ts:116)TValueis genuinely covariant in output-position types; invariance rejects valid library and user code for a marginal winValidation
All re-run after the variance-annotation round:
packages/table-core:pnpm test:types(tsc + declaration emit) ✓,vitest run322/322 ✓, eslint ✓, publint ✓, build ✓ — run uncached vianx --skip-nx-cache.packages/react-table: tsc ✓, eslint ✓, build ✓ (no unit tests in package).angular,vue,solid,svelte,lit,preact):test:types✓ uncached.examples/react/kitchen-sink✓ andexamples/react/custom-plugin✓ (plugin declaration-merging exercises the rewritten options path).Table_Internalno longer carries plugin-merged table API members (plugin options/state still flow). Core code doesn't use plugin APIs and the plugin example only reads merged options, so nothing observable broke; a plugin reading its own merged table APIs throughTable_Internal(rather than the publicTable) would now need a cast.in outdeclares invariance. TypeScript verified the annotations against the measured structure with no contradictions, so today they change nothing semantically — but if a future interface edit makes a parameter genuinely covariant, the annotation would keep it invariant silently. Convention: annotate new generic interfaces and trust the compiler's TS2636 error to catch mistakes.Comparison to the beta.3 checkpoint
tsc --noEmittsc --noEmittsc --noEmit¹ The beta.3 report's alpha baselines differ from today's alpha measurement by ~0.3% (minor alpha commits since that snapshot). react-table and kitchen-sink baselines match exactly.
Profiling notes (for future rounds)
tsc --generateTrace+types.jsonaggregation byfirstDeclarationis the fastest way to localize: top creator in every program isUnionToIntersection's distributed function types (type-utils.ts:15), driven byExtractFeatureMapTypes.createTableHook.tsx,useTable) is highly sensitive to relation-check cost on core types: it doubled the package's check whenTable_Internalrelated structurally, and dropped 60% once variance annotations restored argument-level relation. Watch this benchmark whenever core's central generic types change shape.in out) are only legal on interfaces/classes and on type aliases whose RHS is an object/function/mapped type — not on intersection-with-conditional aliases likeTable(TS2637).Summary by CodeRabbit