perf(router-core): faster enumerable-key collection in structural sharing - #7664

Closed
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/structural-sharing-enumerable-keys
Closed

perf(router-core): faster enumerable-key collection in structural sharing#7664
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/structural-sharing-enumerable-keys

Conversation

@anonrig

@anonriganonrig commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

What

Speed up getEnumerableOwnKeys — the helper that backs replaceEqualDeep (structural sharing) in packages/router-core/src/utils.ts.

It previously called Object.getOwnPropertyNames(o) and then invoked propertyIsEnumerableonce per key to confirm every own string property is enumerable. This swaps that O(n) loop of JS method calls for Object.keys(o) (native, enumerable-only) plus a single length comparison against getOwnPropertyNames(o) to detect non-enumerable own string props. Symbol handling is unchanged.

- const names = Object.getOwnPropertyNames(o)- for (const name of names) {- if (!isEnumerable.call(o, name)) return false- }+ const keys = Object.keys(o)+ if (keys.length !== Object.getOwnPropertyNames(o).length) return false

Why

replaceEqualDeep runs on every selector result on every state update when defaultStructuralSharing is enabled, so getEnumerableOwnKeys is called for every plain object it walks. For selector-heavy apps this is a hot client path (it short-circuits on the server, so this is purely a client-side win).

Benchmark on the real function (Vitest bench, node env where isServer is false), deeply-equal new state object shaped like typical router state (search / params / nested loaderData / context):

ops/s
before~560K
after~762K

1.36× faster in-situ (an isolated A/B of just the key-collection step measured ~1.55×). The win grows with object key count, since the old path made one propertyIsEnumerable call per key.

Correctness

Behavior is identical:

  • Same returned keys in the same order. Object.keys and Object.getOwnPropertyNames follow the same [[OwnPropertyKeys]] ordering (integer indices ascending, then string keys in insertion order), and in the non-bail case every string key is enumerable, so the two orderings coincide.
  • Still bails (returns false) for objects with any non-enumerable own string property (length mismatch) or any non-enumerable own symbol (existing per-symbol check).

Verified by:

  • The existing getEnumerableOwnKeys behavior (via replaceEqualDeep) suite (non-enumerable strings/symbols, frozen/sealed, Object.create(null), numeric keys, shadowing, mixed string+symbol).
  • A 5000+ case fuzz comparing the new key-collection against the old.
  • Two added tests: partial structural sharing on a nested object (unchanged subtrees keep their reference), and a nested object carrying a non-enumerable prop bailing to next.
nx run @tanstack/router-core:test:unit -> 1180 passed (+ 3 expected-fail)
nx run @tanstack/router-core:test:types -> no errors

Notes

  • Single-function change; replaceEqualDeep itself is untouched.
  • Includes a changeset (@tanstack/router-core patch).

Summary by CodeRabbit

  • Performance

    • Improved selector performance through optimized structural sharing computation in router core
  • Tests

    • Added test coverage for structural sharing behavior with nested objects and non-enumerable properties

…ring
getEnumerableOwnKeys (used by replaceEqualDeep) called
Object.getOwnPropertyNames and then invoked propertyIsEnumerable once per
key to verify every own string prop is enumerable. Replace that O(n) loop
of JS method calls with Object.keys (native, enumerable-only) plus a
single length comparison against getOwnPropertyNames to detect
non-enumerable string props. Symbol handling is unchanged.
Behavior is identical (verified against the existing suite + fuzzing): the
function still returns the enumerable own keys in the same order and still
bails (returns false) for objects with any non-enumerable own string or
symbol property.
replaceEqualDeep runs on every selector result on every state update when
defaultStructuralSharing is enabled, so this is a hot client path for
selector-heavy apps. Measured ~1.3-1.5x faster on typical router state.
@coderabbitai

coderabbitaiBot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

getEnumerableOwnKeys in utils.ts is refactored to detect non-enumerable string keys via an Object.keys vs Object.getOwnPropertyNames length comparison instead of iterating and calling propertyIsEnumerable per key. Two new tests for replaceEqualDeep are added, and a patch changeset entry is included.

Changes

Structural Sharing Enumerable Key Optimization

Layer / File(s)Summary
getEnumerableOwnKeys fast-path refactor and changeset
packages/router-core/src/utils.ts, .changeset/perf-structural-sharing-enumerable-keys.md
getEnumerableOwnKeys now compares Object.keys(obj).length against Object.getOwnPropertyNames(obj).length to detect non-enumerable string keys in one shot instead of looping with propertyIsEnumerable; symbol handling is retained. A patch changeset documents the optimization.
replaceEqualDeep identity-sharing and non-enumerable tests
packages/router-core/tests/utils.test.ts
Two new test cases added: one asserting unchanged nested subtree references are preserved when only a leaf value changes, and one asserting that a nested object with a non-enumerable property causes bail-out to the next reference while unchanged siblings retain identity.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • TanStack/router#7577: Refactors React hooks to centralize structural-sharing logic using the same replaceEqualDeep / useStructuralSharing implementation that this PR optimizes.

Poem

🐇 Hop, hop — no more looping through each key,
One length compare sets the fast path free!
Non-enumerable props? We bail with grace,
Identity preserved all over the place.
Object.keys wins this little race! 🗝️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately and concisely summarizes the main change: a performance optimization for enumerable-key collection in the structural sharing mechanism of router-core.
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/router-core/src/utils.ts`:
- Line 308: The if statement checking keys.length against
Object.getOwnPropertyNames(o).length on line 308 uses a one-line body without
curly braces, which violates the repository's style guidelines requiring braces
for all control statements. Add curly braces around the return false statement
to wrap the body. Apply the same fix to the similar if statements also mentioned
on lines 314 and 319.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c38a969f-acca-43e4-97f3-10cdc0a50a17

📥 Commits

Reviewing files that changed from the base of the PR and between 279a849 and d18d300.

📒 Files selected for processing (3)
  • .changeset/perf-structural-sharing-enumerable-keys.md
  • packages/router-core/src/utils.ts
  • packages/router-core/tests/utils.test.ts

// "clone-friendly" -> bail. This replaces an O(n) loop of
// `propertyIsEnumerable` calls with two native calls.
const keys = Object.keys(o)
if (keys.length !== Object.getOwnPropertyNames(o).length) return false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use block bodies for all changed if statements.

These one-line if bodies violate the repository rule requiring braces on control statements.

Suggested patch
- if (keys.length !== Object.getOwnPropertyNames(o).length) return false+ if (keys.length !== Object.getOwnPropertyNames(o).length) {+ return false+ }
@@
- if (symbols.length === 0) return keys+ if (symbols.length === 0) {+ return keys+ }
@@
- if (!isEnumerable.call(o, symbol)) return false+ if (!isEnumerable.call(o, symbol)) {+ return false+ }

As per coding guidelines, "**/*.{ts,tsx,js,jsx}: Always use curly braces for if, else, loops, and similar control statements."

Also applies to: 314-314, 319-319

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/router-core/src/utils.ts` at line 308, The if statement checking
keys.length against Object.getOwnPropertyNames(o).length on line 308 uses a
one-line body without curly braces, which violates the repository's style
guidelines requiring braces for all control statements. Add curly braces around
the return false statement to wrap the body. Apply the same fix to the similar
if statements also mentioned on lines 314 and 319.

Source: Coding guidelines

@nx-cloud

nx-cloudBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit d18d300

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded12m 49sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded2m 14sView ↗

☁️ Nx Cloud last updated this comment at 2026-06-22 11:10:10 UTC

@pkg-pr-new

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7664

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7664

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7664

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7664

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7664

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7664

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7664

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7664

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7664

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7664

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7664

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7664

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7664

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7664

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7664

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7664

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7664

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7664

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7664

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7664

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7664

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7664

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7664

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7664

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7664

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7664

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7664

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7664

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7664

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7664

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7664

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7664

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7664

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7664

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7664

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7664

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7664

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7664

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7664

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7664

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7664

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7664

commit: d18d300

@codspeed-hq

Copy link
Copy Markdown

Merging this PR will degrade performance by 11.21%

⚠️Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
❌ 5 regressed benchmarks
✅ 137 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

ModeBenchmarkBASEHEADEfficiency
Memorymem aborted-requests (solid)1.8 MB2.7 MB-35.01%
Memorymem serialization-payload (solid)6.9 MB8.9 MB-22.2%
Memorymem peak-large-page (solid)3.4 MB3.8 MB-11.32%
Memorymem navigation-churn (solid)1.4 MB1.5 MB-5.76%
Memorymem aborted-requests (vue)959.1 KB989.5 KB-3.07%
Memorymem streaming-peak chunked (vue)12.2 MB11.9 MB+3.1%
Memorymem request-churn (solid)1.2 MB1.1 MB+3.04%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing anonrig:perf/structural-sharing-enumerable-keys (d18d300) with main (f23ed0f)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (279a849) during the generation of this report, so f23ed0f was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Sheraff

Copy link
Copy Markdown
Collaborator

Replacement PR #8010 recreates this change on current main, preserves the original commit authorship, and addresses the outstanding block-body review.

Sign up for freeto 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.

2 participants

@anonrig@Sheraff
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

perf(router-core): faster enumerable-key collection in structural sharing - #7664

Closed
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/structural-sharing-enumerable-keys
Closed

perf(router-core): faster enumerable-key collection in structural sharing#7664
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/structural-sharing-enumerable-keys

Conversation

@anonrig

@anonriganonrig commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

What

Speed up getEnumerableOwnKeys — the helper that backs replaceEqualDeep (structural sharing) in packages/router-core/src/utils.ts.

It previously called Object.getOwnPropertyNames(o) and then invoked propertyIsEnumerableonce per key to confirm every own string property is enumerable. This swaps that O(n) loop of JS method calls for Object.keys(o) (native, enumerable-only) plus a single length comparison against getOwnPropertyNames(o) to detect non-enumerable own string props. Symbol handling is unchanged.

- const names = Object.getOwnPropertyNames(o)- for (const name of names) {- if (!isEnumerable.call(o, name)) return false- }+ const keys = Object.keys(o)+ if (keys.length !== Object.getOwnPropertyNames(o).length) return false

Why

replaceEqualDeep runs on every selector result on every state update when defaultStructuralSharing is enabled, so getEnumerableOwnKeys is called for every plain object it walks. For selector-heavy apps this is a hot client path (it short-circuits on the server, so this is purely a client-side win).

Benchmark on the real function (Vitest bench, node env where isServer is false), deeply-equal new state object shaped like typical router state (search / params / nested loaderData / context):

ops/s
before~560K
after~762K

1.36× faster in-situ (an isolated A/B of just the key-collection step measured ~1.55×). The win grows with object key count, since the old path made one propertyIsEnumerable call per key.

Correctness

Behavior is identical:

  • Same returned keys in the same order. Object.keys and Object.getOwnPropertyNames follow the same [[OwnPropertyKeys]] ordering (integer indices ascending, then string keys in insertion order), and in the non-bail case every string key is enumerable, so the two orderings coincide.
  • Still bails (returns false) for objects with any non-enumerable own string property (length mismatch) or any non-enumerable own symbol (existing per-symbol check).

Verified by:

  • The existing getEnumerableOwnKeys behavior (via replaceEqualDeep) suite (non-enumerable strings/symbols, frozen/sealed, Object.create(null), numeric keys, shadowing, mixed string+symbol).
  • A 5000+ case fuzz comparing the new key-collection against the old.
  • Two added tests: partial structural sharing on a nested object (unchanged subtrees keep their reference), and a nested object carrying a non-enumerable prop bailing to next.
nx run @tanstack/router-core:test:unit -> 1180 passed (+ 3 expected-fail)
nx run @tanstack/router-core:test:types -> no errors

Notes

  • Single-function change; replaceEqualDeep itself is untouched.
  • Includes a changeset (@tanstack/router-core patch).

Summary by CodeRabbit

  • Performance

    • Improved selector performance through optimized structural sharing computation in router core
  • Tests

    • Added test coverage for structural sharing behavior with nested objects and non-enumerable properties

…ring
getEnumerableOwnKeys (used by replaceEqualDeep) called
Object.getOwnPropertyNames and then invoked propertyIsEnumerable once per
key to verify every own string prop is enumerable. Replace that O(n) loop
of JS method calls with Object.keys (native, enumerable-only) plus a
single length comparison against getOwnPropertyNames to detect
non-enumerable string props. Symbol handling is unchanged.
Behavior is identical (verified against the existing suite + fuzzing): the
function still returns the enumerable own keys in the same order and still
bails (returns false) for objects with any non-enumerable own string or
symbol property.
replaceEqualDeep runs on every selector result on every state update when
defaultStructuralSharing is enabled, so this is a hot client path for
selector-heavy apps. Measured ~1.3-1.5x faster on typical router state.
@coderabbitai

coderabbitaiBot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

getEnumerableOwnKeys in utils.ts is refactored to detect non-enumerable string keys via an Object.keys vs Object.getOwnPropertyNames length comparison instead of iterating and calling propertyIsEnumerable per key. Two new tests for replaceEqualDeep are added, and a patch changeset entry is included.

Changes

Structural Sharing Enumerable Key Optimization

Layer / File(s)Summary
getEnumerableOwnKeys fast-path refactor and changeset
packages/router-core/src/utils.ts, .changeset/perf-structural-sharing-enumerable-keys.md
getEnumerableOwnKeys now compares Object.keys(obj).length against Object.getOwnPropertyNames(obj).length to detect non-enumerable string keys in one shot instead of looping with propertyIsEnumerable; symbol handling is retained. A patch changeset documents the optimization.
replaceEqualDeep identity-sharing and non-enumerable tests
packages/router-core/tests/utils.test.ts
Two new test cases added: one asserting unchanged nested subtree references are preserved when only a leaf value changes, and one asserting that a nested object with a non-enumerable property causes bail-out to the next reference while unchanged siblings retain identity.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • TanStack/router#7577: Refactors React hooks to centralize structural-sharing logic using the same replaceEqualDeep / useStructuralSharing implementation that this PR optimizes.

Poem

🐇 Hop, hop — no more looping through each key,
One length compare sets the fast path free!
Non-enumerable props? We bail with grace,
Identity preserved all over the place.
Object.keys wins this little race! 🗝️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately and concisely summarizes the main change: a performance optimization for enumerable-key collection in the structural sharing mechanism of router-core.
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/router-core/src/utils.ts`:
- Line 308: The if statement checking keys.length against
Object.getOwnPropertyNames(o).length on line 308 uses a one-line body without
curly braces, which violates the repository's style guidelines requiring braces
for all control statements. Add curly braces around the return false statement
to wrap the body. Apply the same fix to the similar if statements also mentioned
on lines 314 and 319.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c38a969f-acca-43e4-97f3-10cdc0a50a17

📥 Commits

Reviewing files that changed from the base of the PR and between 279a849 and d18d300.

📒 Files selected for processing (3)
  • .changeset/perf-structural-sharing-enumerable-keys.md
  • packages/router-core/src/utils.ts
  • packages/router-core/tests/utils.test.ts

// "clone-friendly" -> bail. This replaces an O(n) loop of
// `propertyIsEnumerable` calls with two native calls.
const keys = Object.keys(o)
if (keys.length !== Object.getOwnPropertyNames(o).length) return false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use block bodies for all changed if statements.

These one-line if bodies violate the repository rule requiring braces on control statements.

Suggested patch
- if (keys.length !== Object.getOwnPropertyNames(o).length) return false+ if (keys.length !== Object.getOwnPropertyNames(o).length) {+ return false+ }
@@
- if (symbols.length === 0) return keys+ if (symbols.length === 0) {+ return keys+ }
@@
- if (!isEnumerable.call(o, symbol)) return false+ if (!isEnumerable.call(o, symbol)) {+ return false+ }

As per coding guidelines, "**/*.{ts,tsx,js,jsx}: Always use curly braces for if, else, loops, and similar control statements."

Also applies to: 314-314, 319-319

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/router-core/src/utils.ts` at line 308, The if statement checking
keys.length against Object.getOwnPropertyNames(o).length on line 308 uses a
one-line body without curly braces, which violates the repository's style
guidelines requiring braces for all control statements. Add curly braces around
the return false statement to wrap the body. Apply the same fix to the similar
if statements also mentioned on lines 314 and 319.

Source: Coding guidelines

@nx-cloud

nx-cloudBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit d18d300

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded12m 49sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded2m 14sView ↗

☁️ Nx Cloud last updated this comment at 2026-06-22 11:10:10 UTC

@pkg-pr-new

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7664

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7664

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7664

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7664

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7664

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7664

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7664

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7664

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7664

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7664

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7664

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7664

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7664

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7664

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7664

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7664

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7664

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7664

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7664

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7664

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7664

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7664

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7664

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7664

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7664

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7664

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7664

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7664

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7664

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7664

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7664

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7664

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7664

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7664

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7664

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7664

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7664

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7664

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7664

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7664

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7664

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7664

commit: d18d300

@codspeed-hq

Copy link
Copy Markdown

Merging this PR will degrade performance by 11.21%

⚠️Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
❌ 5 regressed benchmarks
✅ 137 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

ModeBenchmarkBASEHEADEfficiency
Memorymem aborted-requests (solid)1.8 MB2.7 MB-35.01%
Memorymem serialization-payload (solid)6.9 MB8.9 MB-22.2%
Memorymem peak-large-page (solid)3.4 MB3.8 MB-11.32%
Memorymem navigation-churn (solid)1.4 MB1.5 MB-5.76%
Memorymem aborted-requests (vue)959.1 KB989.5 KB-3.07%
Memorymem streaming-peak chunked (vue)12.2 MB11.9 MB+3.1%
Memorymem request-churn (solid)1.2 MB1.1 MB+3.04%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing anonrig:perf/structural-sharing-enumerable-keys (d18d300) with main (f23ed0f)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (279a849) during the generation of this report, so f23ed0f was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Sheraff

Copy link
Copy Markdown
Collaborator

Replacement PR #8010 recreates this change on current main, preserves the original commit authorship, and addresses the outstanding block-body review.

Sign up for freeto 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.

2 participants

@anonrig@Sheraff
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

perf(router-core): faster enumerable-key collection in structural sharing - #7664

Closed
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/structural-sharing-enumerable-keys
Closed

perf(router-core): faster enumerable-key collection in structural sharing#7664
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/structural-sharing-enumerable-keys

Conversation

@anonrig

@anonriganonrig commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

What

Speed up getEnumerableOwnKeys — the helper that backs replaceEqualDeep (structural sharing) in packages/router-core/src/utils.ts.

It previously called Object.getOwnPropertyNames(o) and then invoked propertyIsEnumerableonce per key to confirm every own string property is enumerable. This swaps that O(n) loop of JS method calls for Object.keys(o) (native, enumerable-only) plus a single length comparison against getOwnPropertyNames(o) to detect non-enumerable own string props. Symbol handling is unchanged.

- const names = Object.getOwnPropertyNames(o)- for (const name of names) {- if (!isEnumerable.call(o, name)) return false- }+ const keys = Object.keys(o)+ if (keys.length !== Object.getOwnPropertyNames(o).length) return false

Why

replaceEqualDeep runs on every selector result on every state update when defaultStructuralSharing is enabled, so getEnumerableOwnKeys is called for every plain object it walks. For selector-heavy apps this is a hot client path (it short-circuits on the server, so this is purely a client-side win).

Benchmark on the real function (Vitest bench, node env where isServer is false), deeply-equal new state object shaped like typical router state (search / params / nested loaderData / context):

ops/s
before~560K
after~762K

1.36× faster in-situ (an isolated A/B of just the key-collection step measured ~1.55×). The win grows with object key count, since the old path made one propertyIsEnumerable call per key.

Correctness

Behavior is identical:

  • Same returned keys in the same order. Object.keys and Object.getOwnPropertyNames follow the same [[OwnPropertyKeys]] ordering (integer indices ascending, then string keys in insertion order), and in the non-bail case every string key is enumerable, so the two orderings coincide.
  • Still bails (returns false) for objects with any non-enumerable own string property (length mismatch) or any non-enumerable own symbol (existing per-symbol check).

Verified by:

  • The existing getEnumerableOwnKeys behavior (via replaceEqualDeep) suite (non-enumerable strings/symbols, frozen/sealed, Object.create(null), numeric keys, shadowing, mixed string+symbol).
  • A 5000+ case fuzz comparing the new key-collection against the old.
  • Two added tests: partial structural sharing on a nested object (unchanged subtrees keep their reference), and a nested object carrying a non-enumerable prop bailing to next.
nx run @tanstack/router-core:test:unit -> 1180 passed (+ 3 expected-fail)
nx run @tanstack/router-core:test:types -> no errors

Notes

  • Single-function change; replaceEqualDeep itself is untouched.
  • Includes a changeset (@tanstack/router-core patch).

Summary by CodeRabbit

  • Performance

    • Improved selector performance through optimized structural sharing computation in router core
  • Tests

    • Added test coverage for structural sharing behavior with nested objects and non-enumerable properties

…ring
getEnumerableOwnKeys (used by replaceEqualDeep) called
Object.getOwnPropertyNames and then invoked propertyIsEnumerable once per
key to verify every own string prop is enumerable. Replace that O(n) loop
of JS method calls with Object.keys (native, enumerable-only) plus a
single length comparison against getOwnPropertyNames to detect
non-enumerable string props. Symbol handling is unchanged.
Behavior is identical (verified against the existing suite + fuzzing): the
function still returns the enumerable own keys in the same order and still
bails (returns false) for objects with any non-enumerable own string or
symbol property.
replaceEqualDeep runs on every selector result on every state update when
defaultStructuralSharing is enabled, so this is a hot client path for
selector-heavy apps. Measured ~1.3-1.5x faster on typical router state.
@coderabbitai

coderabbitaiBot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

getEnumerableOwnKeys in utils.ts is refactored to detect non-enumerable string keys via an Object.keys vs Object.getOwnPropertyNames length comparison instead of iterating and calling propertyIsEnumerable per key. Two new tests for replaceEqualDeep are added, and a patch changeset entry is included.

Changes

Structural Sharing Enumerable Key Optimization

Layer / File(s)Summary
getEnumerableOwnKeys fast-path refactor and changeset
packages/router-core/src/utils.ts, .changeset/perf-structural-sharing-enumerable-keys.md
getEnumerableOwnKeys now compares Object.keys(obj).length against Object.getOwnPropertyNames(obj).length to detect non-enumerable string keys in one shot instead of looping with propertyIsEnumerable; symbol handling is retained. A patch changeset documents the optimization.
replaceEqualDeep identity-sharing and non-enumerable tests
packages/router-core/tests/utils.test.ts
Two new test cases added: one asserting unchanged nested subtree references are preserved when only a leaf value changes, and one asserting that a nested object with a non-enumerable property causes bail-out to the next reference while unchanged siblings retain identity.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • TanStack/router#7577: Refactors React hooks to centralize structural-sharing logic using the same replaceEqualDeep / useStructuralSharing implementation that this PR optimizes.

Poem

🐇 Hop, hop — no more looping through each key,
One length compare sets the fast path free!
Non-enumerable props? We bail with grace,
Identity preserved all over the place.
Object.keys wins this little race! 🗝️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately and concisely summarizes the main change: a performance optimization for enumerable-key collection in the structural sharing mechanism of router-core.
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/router-core/src/utils.ts`:
- Line 308: The if statement checking keys.length against
Object.getOwnPropertyNames(o).length on line 308 uses a one-line body without
curly braces, which violates the repository's style guidelines requiring braces
for all control statements. Add curly braces around the return false statement
to wrap the body. Apply the same fix to the similar if statements also mentioned
on lines 314 and 319.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c38a969f-acca-43e4-97f3-10cdc0a50a17

📥 Commits

Reviewing files that changed from the base of the PR and between 279a849 and d18d300.

📒 Files selected for processing (3)
  • .changeset/perf-structural-sharing-enumerable-keys.md
  • packages/router-core/src/utils.ts
  • packages/router-core/tests/utils.test.ts

// "clone-friendly" -> bail. This replaces an O(n) loop of
// `propertyIsEnumerable` calls with two native calls.
const keys = Object.keys(o)
if (keys.length !== Object.getOwnPropertyNames(o).length) return false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use block bodies for all changed if statements.

These one-line if bodies violate the repository rule requiring braces on control statements.

Suggested patch
- if (keys.length !== Object.getOwnPropertyNames(o).length) return false+ if (keys.length !== Object.getOwnPropertyNames(o).length) {+ return false+ }
@@
- if (symbols.length === 0) return keys+ if (symbols.length === 0) {+ return keys+ }
@@
- if (!isEnumerable.call(o, symbol)) return false+ if (!isEnumerable.call(o, symbol)) {+ return false+ }

As per coding guidelines, "**/*.{ts,tsx,js,jsx}: Always use curly braces for if, else, loops, and similar control statements."

Also applies to: 314-314, 319-319

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/router-core/src/utils.ts` at line 308, The if statement checking
keys.length against Object.getOwnPropertyNames(o).length on line 308 uses a
one-line body without curly braces, which violates the repository's style
guidelines requiring braces for all control statements. Add curly braces around
the return false statement to wrap the body. Apply the same fix to the similar
if statements also mentioned on lines 314 and 319.

Source: Coding guidelines

@nx-cloud

nx-cloudBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit d18d300

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded12m 49sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded2m 14sView ↗

☁️ Nx Cloud last updated this comment at 2026-06-22 11:10:10 UTC

@pkg-pr-new

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7664

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7664

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7664

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7664

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7664

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7664

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7664

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7664

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7664

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7664

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7664

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7664

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7664

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7664

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7664

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7664

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7664

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7664

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7664

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7664

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7664

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7664

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7664

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7664

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7664

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7664

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7664

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7664

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7664

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7664

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7664

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7664

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7664

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7664

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7664

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7664

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7664

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7664

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7664

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7664

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7664

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7664

commit: d18d300

@codspeed-hq

Copy link
Copy Markdown

Merging this PR will degrade performance by 11.21%

⚠️Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
❌ 5 regressed benchmarks
✅ 137 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

ModeBenchmarkBASEHEADEfficiency
Memorymem aborted-requests (solid)1.8 MB2.7 MB-35.01%
Memorymem serialization-payload (solid)6.9 MB8.9 MB-22.2%
Memorymem peak-large-page (solid)3.4 MB3.8 MB-11.32%
Memorymem navigation-churn (solid)1.4 MB1.5 MB-5.76%
Memorymem aborted-requests (vue)959.1 KB989.5 KB-3.07%
Memorymem streaming-peak chunked (vue)12.2 MB11.9 MB+3.1%
Memorymem request-churn (solid)1.2 MB1.1 MB+3.04%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing anonrig:perf/structural-sharing-enumerable-keys (d18d300) with main (f23ed0f)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (279a849) during the generation of this report, so f23ed0f was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Sheraff

Copy link
Copy Markdown
Collaborator

Replacement PR #8010 recreates this change on current main, preserves the original commit authorship, and addresses the outstanding block-body review.

Sign up for freeto 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.

2 participants

@anonrig@Sheraff
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

perf(router-core): faster enumerable-key collection in structural sharing - #7664

Closed
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/structural-sharing-enumerable-keys
Closed

perf(router-core): faster enumerable-key collection in structural sharing#7664
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/structural-sharing-enumerable-keys

Conversation

@anonrig

@anonriganonrig commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

What

Speed up getEnumerableOwnKeys — the helper that backs replaceEqualDeep (structural sharing) in packages/router-core/src/utils.ts.

It previously called Object.getOwnPropertyNames(o) and then invoked propertyIsEnumerableonce per key to confirm every own string property is enumerable. This swaps that O(n) loop of JS method calls for Object.keys(o) (native, enumerable-only) plus a single length comparison against getOwnPropertyNames(o) to detect non-enumerable own string props. Symbol handling is unchanged.

- const names = Object.getOwnPropertyNames(o)- for (const name of names) {- if (!isEnumerable.call(o, name)) return false- }+ const keys = Object.keys(o)+ if (keys.length !== Object.getOwnPropertyNames(o).length) return false

Why

replaceEqualDeep runs on every selector result on every state update when defaultStructuralSharing is enabled, so getEnumerableOwnKeys is called for every plain object it walks. For selector-heavy apps this is a hot client path (it short-circuits on the server, so this is purely a client-side win).

Benchmark on the real function (Vitest bench, node env where isServer is false), deeply-equal new state object shaped like typical router state (search / params / nested loaderData / context):

ops/s
before~560K
after~762K

1.36× faster in-situ (an isolated A/B of just the key-collection step measured ~1.55×). The win grows with object key count, since the old path made one propertyIsEnumerable call per key.

Correctness

Behavior is identical:

  • Same returned keys in the same order. Object.keys and Object.getOwnPropertyNames follow the same [[OwnPropertyKeys]] ordering (integer indices ascending, then string keys in insertion order), and in the non-bail case every string key is enumerable, so the two orderings coincide.
  • Still bails (returns false) for objects with any non-enumerable own string property (length mismatch) or any non-enumerable own symbol (existing per-symbol check).

Verified by:

  • The existing getEnumerableOwnKeys behavior (via replaceEqualDeep) suite (non-enumerable strings/symbols, frozen/sealed, Object.create(null), numeric keys, shadowing, mixed string+symbol).
  • A 5000+ case fuzz comparing the new key-collection against the old.
  • Two added tests: partial structural sharing on a nested object (unchanged subtrees keep their reference), and a nested object carrying a non-enumerable prop bailing to next.
nx run @tanstack/router-core:test:unit -> 1180 passed (+ 3 expected-fail)
nx run @tanstack/router-core:test:types -> no errors

Notes

  • Single-function change; replaceEqualDeep itself is untouched.
  • Includes a changeset (@tanstack/router-core patch).

Summary by CodeRabbit

  • Performance

    • Improved selector performance through optimized structural sharing computation in router core
  • Tests

    • Added test coverage for structural sharing behavior with nested objects and non-enumerable properties

…ring
getEnumerableOwnKeys (used by replaceEqualDeep) called
Object.getOwnPropertyNames and then invoked propertyIsEnumerable once per
key to verify every own string prop is enumerable. Replace that O(n) loop
of JS method calls with Object.keys (native, enumerable-only) plus a
single length comparison against getOwnPropertyNames to detect
non-enumerable string props. Symbol handling is unchanged.
Behavior is identical (verified against the existing suite + fuzzing): the
function still returns the enumerable own keys in the same order and still
bails (returns false) for objects with any non-enumerable own string or
symbol property.
replaceEqualDeep runs on every selector result on every state update when
defaultStructuralSharing is enabled, so this is a hot client path for
selector-heavy apps. Measured ~1.3-1.5x faster on typical router state.
@coderabbitai

coderabbitaiBot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

getEnumerableOwnKeys in utils.ts is refactored to detect non-enumerable string keys via an Object.keys vs Object.getOwnPropertyNames length comparison instead of iterating and calling propertyIsEnumerable per key. Two new tests for replaceEqualDeep are added, and a patch changeset entry is included.

Changes

Structural Sharing Enumerable Key Optimization

Layer / File(s)Summary
getEnumerableOwnKeys fast-path refactor and changeset
packages/router-core/src/utils.ts, .changeset/perf-structural-sharing-enumerable-keys.md
getEnumerableOwnKeys now compares Object.keys(obj).length against Object.getOwnPropertyNames(obj).length to detect non-enumerable string keys in one shot instead of looping with propertyIsEnumerable; symbol handling is retained. A patch changeset documents the optimization.
replaceEqualDeep identity-sharing and non-enumerable tests
packages/router-core/tests/utils.test.ts
Two new test cases added: one asserting unchanged nested subtree references are preserved when only a leaf value changes, and one asserting that a nested object with a non-enumerable property causes bail-out to the next reference while unchanged siblings retain identity.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • TanStack/router#7577: Refactors React hooks to centralize structural-sharing logic using the same replaceEqualDeep / useStructuralSharing implementation that this PR optimizes.

Poem

🐇 Hop, hop — no more looping through each key,
One length compare sets the fast path free!
Non-enumerable props? We bail with grace,
Identity preserved all over the place.
Object.keys wins this little race! 🗝️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately and concisely summarizes the main change: a performance optimization for enumerable-key collection in the structural sharing mechanism of router-core.
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/router-core/src/utils.ts`:
- Line 308: The if statement checking keys.length against
Object.getOwnPropertyNames(o).length on line 308 uses a one-line body without
curly braces, which violates the repository's style guidelines requiring braces
for all control statements. Add curly braces around the return false statement
to wrap the body. Apply the same fix to the similar if statements also mentioned
on lines 314 and 319.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c38a969f-acca-43e4-97f3-10cdc0a50a17

📥 Commits

Reviewing files that changed from the base of the PR and between 279a849 and d18d300.

📒 Files selected for processing (3)
  • .changeset/perf-structural-sharing-enumerable-keys.md
  • packages/router-core/src/utils.ts
  • packages/router-core/tests/utils.test.ts

// "clone-friendly" -> bail. This replaces an O(n) loop of
// `propertyIsEnumerable` calls with two native calls.
const keys = Object.keys(o)
if (keys.length !== Object.getOwnPropertyNames(o).length) return false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use block bodies for all changed if statements.

These one-line if bodies violate the repository rule requiring braces on control statements.

Suggested patch
- if (keys.length !== Object.getOwnPropertyNames(o).length) return false+ if (keys.length !== Object.getOwnPropertyNames(o).length) {+ return false+ }
@@
- if (symbols.length === 0) return keys+ if (symbols.length === 0) {+ return keys+ }
@@
- if (!isEnumerable.call(o, symbol)) return false+ if (!isEnumerable.call(o, symbol)) {+ return false+ }

As per coding guidelines, "**/*.{ts,tsx,js,jsx}: Always use curly braces for if, else, loops, and similar control statements."

Also applies to: 314-314, 319-319

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/router-core/src/utils.ts` at line 308, The if statement checking
keys.length against Object.getOwnPropertyNames(o).length on line 308 uses a
one-line body without curly braces, which violates the repository's style
guidelines requiring braces for all control statements. Add curly braces around
the return false statement to wrap the body. Apply the same fix to the similar
if statements also mentioned on lines 314 and 319.

Source: Coding guidelines

@nx-cloud

nx-cloudBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit d18d300

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded12m 49sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded2m 14sView ↗

☁️ Nx Cloud last updated this comment at 2026-06-22 11:10:10 UTC

@pkg-pr-new

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7664

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7664

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7664

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7664

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7664

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7664

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7664

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7664

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7664

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7664

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7664

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7664

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7664

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7664

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7664

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7664

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7664

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7664

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7664

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7664

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7664

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7664

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7664

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7664

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7664

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7664

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7664

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7664

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7664

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7664

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7664

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7664

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7664

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7664

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7664

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7664

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7664

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7664

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7664

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7664

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7664

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7664

commit: d18d300

@codspeed-hq

Copy link
Copy Markdown

Merging this PR will degrade performance by 11.21%

⚠️Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
❌ 5 regressed benchmarks
✅ 137 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

ModeBenchmarkBASEHEADEfficiency
Memorymem aborted-requests (solid)1.8 MB2.7 MB-35.01%
Memorymem serialization-payload (solid)6.9 MB8.9 MB-22.2%
Memorymem peak-large-page (solid)3.4 MB3.8 MB-11.32%
Memorymem navigation-churn (solid)1.4 MB1.5 MB-5.76%
Memorymem aborted-requests (vue)959.1 KB989.5 KB-3.07%
Memorymem streaming-peak chunked (vue)12.2 MB11.9 MB+3.1%
Memorymem request-churn (solid)1.2 MB1.1 MB+3.04%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing anonrig:perf/structural-sharing-enumerable-keys (d18d300) with main (f23ed0f)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (279a849) during the generation of this report, so f23ed0f was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Sheraff

Copy link
Copy Markdown
Collaborator

Replacement PR #8010 recreates this change on current main, preserves the original commit authorship, and addresses the outstanding block-body review.

Sign up for freeto 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.

2 participants

@anonrig@Sheraff
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

perf(router-core): faster enumerable-key collection in structural sharing - #7664

Closed
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/structural-sharing-enumerable-keys
Closed

perf(router-core): faster enumerable-key collection in structural sharing#7664
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/structural-sharing-enumerable-keys

Conversation

@anonrig

@anonriganonrig commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

What

Speed up getEnumerableOwnKeys — the helper that backs replaceEqualDeep (structural sharing) in packages/router-core/src/utils.ts.

It previously called Object.getOwnPropertyNames(o) and then invoked propertyIsEnumerableonce per key to confirm every own string property is enumerable. This swaps that O(n) loop of JS method calls for Object.keys(o) (native, enumerable-only) plus a single length comparison against getOwnPropertyNames(o) to detect non-enumerable own string props. Symbol handling is unchanged.

- const names = Object.getOwnPropertyNames(o)- for (const name of names) {- if (!isEnumerable.call(o, name)) return false- }+ const keys = Object.keys(o)+ if (keys.length !== Object.getOwnPropertyNames(o).length) return false

Why

replaceEqualDeep runs on every selector result on every state update when defaultStructuralSharing is enabled, so getEnumerableOwnKeys is called for every plain object it walks. For selector-heavy apps this is a hot client path (it short-circuits on the server, so this is purely a client-side win).

Benchmark on the real function (Vitest bench, node env where isServer is false), deeply-equal new state object shaped like typical router state (search / params / nested loaderData / context):

ops/s
before~560K
after~762K

1.36× faster in-situ (an isolated A/B of just the key-collection step measured ~1.55×). The win grows with object key count, since the old path made one propertyIsEnumerable call per key.

Correctness

Behavior is identical:

  • Same returned keys in the same order. Object.keys and Object.getOwnPropertyNames follow the same [[OwnPropertyKeys]] ordering (integer indices ascending, then string keys in insertion order), and in the non-bail case every string key is enumerable, so the two orderings coincide.
  • Still bails (returns false) for objects with any non-enumerable own string property (length mismatch) or any non-enumerable own symbol (existing per-symbol check).

Verified by:

  • The existing getEnumerableOwnKeys behavior (via replaceEqualDeep) suite (non-enumerable strings/symbols, frozen/sealed, Object.create(null), numeric keys, shadowing, mixed string+symbol).
  • A 5000+ case fuzz comparing the new key-collection against the old.
  • Two added tests: partial structural sharing on a nested object (unchanged subtrees keep their reference), and a nested object carrying a non-enumerable prop bailing to next.
nx run @tanstack/router-core:test:unit -> 1180 passed (+ 3 expected-fail)
nx run @tanstack/router-core:test:types -> no errors

Notes

  • Single-function change; replaceEqualDeep itself is untouched.
  • Includes a changeset (@tanstack/router-core patch).

Summary by CodeRabbit

  • Performance

    • Improved selector performance through optimized structural sharing computation in router core
  • Tests

    • Added test coverage for structural sharing behavior with nested objects and non-enumerable properties

…ring
getEnumerableOwnKeys (used by replaceEqualDeep) called
Object.getOwnPropertyNames and then invoked propertyIsEnumerable once per
key to verify every own string prop is enumerable. Replace that O(n) loop
of JS method calls with Object.keys (native, enumerable-only) plus a
single length comparison against getOwnPropertyNames to detect
non-enumerable string props. Symbol handling is unchanged.
Behavior is identical (verified against the existing suite + fuzzing): the
function still returns the enumerable own keys in the same order and still
bails (returns false) for objects with any non-enumerable own string or
symbol property.
replaceEqualDeep runs on every selector result on every state update when
defaultStructuralSharing is enabled, so this is a hot client path for
selector-heavy apps. Measured ~1.3-1.5x faster on typical router state.
@coderabbitai

coderabbitaiBot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

getEnumerableOwnKeys in utils.ts is refactored to detect non-enumerable string keys via an Object.keys vs Object.getOwnPropertyNames length comparison instead of iterating and calling propertyIsEnumerable per key. Two new tests for replaceEqualDeep are added, and a patch changeset entry is included.

Changes

Structural Sharing Enumerable Key Optimization

Layer / File(s)Summary
getEnumerableOwnKeys fast-path refactor and changeset
packages/router-core/src/utils.ts, .changeset/perf-structural-sharing-enumerable-keys.md
getEnumerableOwnKeys now compares Object.keys(obj).length against Object.getOwnPropertyNames(obj).length to detect non-enumerable string keys in one shot instead of looping with propertyIsEnumerable; symbol handling is retained. A patch changeset documents the optimization.
replaceEqualDeep identity-sharing and non-enumerable tests
packages/router-core/tests/utils.test.ts
Two new test cases added: one asserting unchanged nested subtree references are preserved when only a leaf value changes, and one asserting that a nested object with a non-enumerable property causes bail-out to the next reference while unchanged siblings retain identity.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • TanStack/router#7577: Refactors React hooks to centralize structural-sharing logic using the same replaceEqualDeep / useStructuralSharing implementation that this PR optimizes.

Poem

🐇 Hop, hop — no more looping through each key,
One length compare sets the fast path free!
Non-enumerable props? We bail with grace,
Identity preserved all over the place.
Object.keys wins this little race! 🗝️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately and concisely summarizes the main change: a performance optimization for enumerable-key collection in the structural sharing mechanism of router-core.
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/router-core/src/utils.ts`:
- Line 308: The if statement checking keys.length against
Object.getOwnPropertyNames(o).length on line 308 uses a one-line body without
curly braces, which violates the repository's style guidelines requiring braces
for all control statements. Add curly braces around the return false statement
to wrap the body. Apply the same fix to the similar if statements also mentioned
on lines 314 and 319.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c38a969f-acca-43e4-97f3-10cdc0a50a17

📥 Commits

Reviewing files that changed from the base of the PR and between 279a849 and d18d300.

📒 Files selected for processing (3)
  • .changeset/perf-structural-sharing-enumerable-keys.md
  • packages/router-core/src/utils.ts
  • packages/router-core/tests/utils.test.ts

// "clone-friendly" -> bail. This replaces an O(n) loop of
// `propertyIsEnumerable` calls with two native calls.
const keys = Object.keys(o)
if (keys.length !== Object.getOwnPropertyNames(o).length) return false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use block bodies for all changed if statements.

These one-line if bodies violate the repository rule requiring braces on control statements.

Suggested patch
- if (keys.length !== Object.getOwnPropertyNames(o).length) return false+ if (keys.length !== Object.getOwnPropertyNames(o).length) {+ return false+ }
@@
- if (symbols.length === 0) return keys+ if (symbols.length === 0) {+ return keys+ }
@@
- if (!isEnumerable.call(o, symbol)) return false+ if (!isEnumerable.call(o, symbol)) {+ return false+ }

As per coding guidelines, "**/*.{ts,tsx,js,jsx}: Always use curly braces for if, else, loops, and similar control statements."

Also applies to: 314-314, 319-319

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/router-core/src/utils.ts` at line 308, The if statement checking
keys.length against Object.getOwnPropertyNames(o).length on line 308 uses a
one-line body without curly braces, which violates the repository's style
guidelines requiring braces for all control statements. Add curly braces around
the return false statement to wrap the body. Apply the same fix to the similar
if statements also mentioned on lines 314 and 319.

Source: Coding guidelines

@nx-cloud

nx-cloudBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit d18d300

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded12m 49sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded2m 14sView ↗

☁️ Nx Cloud last updated this comment at 2026-06-22 11:10:10 UTC

@pkg-pr-new

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7664

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7664

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7664

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7664

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7664

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7664

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7664

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7664

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7664

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7664

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7664

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7664

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7664

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7664

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7664

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7664

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7664

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7664

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7664

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7664

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7664

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7664

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7664

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7664

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7664

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7664

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7664

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7664

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7664

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7664

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7664

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7664

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7664

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7664

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7664

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7664

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7664

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7664

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7664

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7664

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7664

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7664

commit: d18d300

@codspeed-hq

Copy link
Copy Markdown

Merging this PR will degrade performance by 11.21%

⚠️Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
❌ 5 regressed benchmarks
✅ 137 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

ModeBenchmarkBASEHEADEfficiency
Memorymem aborted-requests (solid)1.8 MB2.7 MB-35.01%
Memorymem serialization-payload (solid)6.9 MB8.9 MB-22.2%
Memorymem peak-large-page (solid)3.4 MB3.8 MB-11.32%
Memorymem navigation-churn (solid)1.4 MB1.5 MB-5.76%
Memorymem aborted-requests (vue)959.1 KB989.5 KB-3.07%
Memorymem streaming-peak chunked (vue)12.2 MB11.9 MB+3.1%
Memorymem request-churn (solid)1.2 MB1.1 MB+3.04%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing anonrig:perf/structural-sharing-enumerable-keys (d18d300) with main (f23ed0f)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (279a849) during the generation of this report, so f23ed0f was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Sheraff

Copy link
Copy Markdown
Collaborator

Replacement PR #8010 recreates this change on current main, preserves the original commit authorship, and addresses the outstanding block-body review.

Sign up for freeto 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.

2 participants

@anonrig@Sheraff
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

perf(router-core): faster enumerable-key collection in structural sharing - #7664

Closed
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/structural-sharing-enumerable-keys
Closed

perf(router-core): faster enumerable-key collection in structural sharing#7664
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/structural-sharing-enumerable-keys

Conversation

@anonrig

@anonriganonrig commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

What

Speed up getEnumerableOwnKeys — the helper that backs replaceEqualDeep (structural sharing) in packages/router-core/src/utils.ts.

It previously called Object.getOwnPropertyNames(o) and then invoked propertyIsEnumerableonce per key to confirm every own string property is enumerable. This swaps that O(n) loop of JS method calls for Object.keys(o) (native, enumerable-only) plus a single length comparison against getOwnPropertyNames(o) to detect non-enumerable own string props. Symbol handling is unchanged.

- const names = Object.getOwnPropertyNames(o)- for (const name of names) {- if (!isEnumerable.call(o, name)) return false- }+ const keys = Object.keys(o)+ if (keys.length !== Object.getOwnPropertyNames(o).length) return false

Why

replaceEqualDeep runs on every selector result on every state update when defaultStructuralSharing is enabled, so getEnumerableOwnKeys is called for every plain object it walks. For selector-heavy apps this is a hot client path (it short-circuits on the server, so this is purely a client-side win).

Benchmark on the real function (Vitest bench, node env where isServer is false), deeply-equal new state object shaped like typical router state (search / params / nested loaderData / context):

ops/s
before~560K
after~762K

1.36× faster in-situ (an isolated A/B of just the key-collection step measured ~1.55×). The win grows with object key count, since the old path made one propertyIsEnumerable call per key.

Correctness

Behavior is identical:

  • Same returned keys in the same order. Object.keys and Object.getOwnPropertyNames follow the same [[OwnPropertyKeys]] ordering (integer indices ascending, then string keys in insertion order), and in the non-bail case every string key is enumerable, so the two orderings coincide.
  • Still bails (returns false) for objects with any non-enumerable own string property (length mismatch) or any non-enumerable own symbol (existing per-symbol check).

Verified by:

  • The existing getEnumerableOwnKeys behavior (via replaceEqualDeep) suite (non-enumerable strings/symbols, frozen/sealed, Object.create(null), numeric keys, shadowing, mixed string+symbol).
  • A 5000+ case fuzz comparing the new key-collection against the old.
  • Two added tests: partial structural sharing on a nested object (unchanged subtrees keep their reference), and a nested object carrying a non-enumerable prop bailing to next.
nx run @tanstack/router-core:test:unit -> 1180 passed (+ 3 expected-fail)
nx run @tanstack/router-core:test:types -> no errors

Notes

  • Single-function change; replaceEqualDeep itself is untouched.
  • Includes a changeset (@tanstack/router-core patch).

Summary by CodeRabbit

  • Performance

    • Improved selector performance through optimized structural sharing computation in router core
  • Tests

    • Added test coverage for structural sharing behavior with nested objects and non-enumerable properties

…ring
getEnumerableOwnKeys (used by replaceEqualDeep) called
Object.getOwnPropertyNames and then invoked propertyIsEnumerable once per
key to verify every own string prop is enumerable. Replace that O(n) loop
of JS method calls with Object.keys (native, enumerable-only) plus a
single length comparison against getOwnPropertyNames to detect
non-enumerable string props. Symbol handling is unchanged.
Behavior is identical (verified against the existing suite + fuzzing): the
function still returns the enumerable own keys in the same order and still
bails (returns false) for objects with any non-enumerable own string or
symbol property.
replaceEqualDeep runs on every selector result on every state update when
defaultStructuralSharing is enabled, so this is a hot client path for
selector-heavy apps. Measured ~1.3-1.5x faster on typical router state.
@coderabbitai

coderabbitaiBot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

getEnumerableOwnKeys in utils.ts is refactored to detect non-enumerable string keys via an Object.keys vs Object.getOwnPropertyNames length comparison instead of iterating and calling propertyIsEnumerable per key. Two new tests for replaceEqualDeep are added, and a patch changeset entry is included.

Changes

Structural Sharing Enumerable Key Optimization

Layer / File(s)Summary
getEnumerableOwnKeys fast-path refactor and changeset
packages/router-core/src/utils.ts, .changeset/perf-structural-sharing-enumerable-keys.md
getEnumerableOwnKeys now compares Object.keys(obj).length against Object.getOwnPropertyNames(obj).length to detect non-enumerable string keys in one shot instead of looping with propertyIsEnumerable; symbol handling is retained. A patch changeset documents the optimization.
replaceEqualDeep identity-sharing and non-enumerable tests
packages/router-core/tests/utils.test.ts
Two new test cases added: one asserting unchanged nested subtree references are preserved when only a leaf value changes, and one asserting that a nested object with a non-enumerable property causes bail-out to the next reference while unchanged siblings retain identity.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • TanStack/router#7577: Refactors React hooks to centralize structural-sharing logic using the same replaceEqualDeep / useStructuralSharing implementation that this PR optimizes.

Poem

🐇 Hop, hop — no more looping through each key,
One length compare sets the fast path free!
Non-enumerable props? We bail with grace,
Identity preserved all over the place.
Object.keys wins this little race! 🗝️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately and concisely summarizes the main change: a performance optimization for enumerable-key collection in the structural sharing mechanism of router-core.
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/router-core/src/utils.ts`:
- Line 308: The if statement checking keys.length against
Object.getOwnPropertyNames(o).length on line 308 uses a one-line body without
curly braces, which violates the repository's style guidelines requiring braces
for all control statements. Add curly braces around the return false statement
to wrap the body. Apply the same fix to the similar if statements also mentioned
on lines 314 and 319.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c38a969f-acca-43e4-97f3-10cdc0a50a17

📥 Commits

Reviewing files that changed from the base of the PR and between 279a849 and d18d300.

📒 Files selected for processing (3)
  • .changeset/perf-structural-sharing-enumerable-keys.md
  • packages/router-core/src/utils.ts
  • packages/router-core/tests/utils.test.ts

// "clone-friendly" -> bail. This replaces an O(n) loop of
// `propertyIsEnumerable` calls with two native calls.
const keys = Object.keys(o)
if (keys.length !== Object.getOwnPropertyNames(o).length) return false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use block bodies for all changed if statements.

These one-line if bodies violate the repository rule requiring braces on control statements.

Suggested patch
- if (keys.length !== Object.getOwnPropertyNames(o).length) return false+ if (keys.length !== Object.getOwnPropertyNames(o).length) {+ return false+ }
@@
- if (symbols.length === 0) return keys+ if (symbols.length === 0) {+ return keys+ }
@@
- if (!isEnumerable.call(o, symbol)) return false+ if (!isEnumerable.call(o, symbol)) {+ return false+ }

As per coding guidelines, "**/*.{ts,tsx,js,jsx}: Always use curly braces for if, else, loops, and similar control statements."

Also applies to: 314-314, 319-319

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/router-core/src/utils.ts` at line 308, The if statement checking
keys.length against Object.getOwnPropertyNames(o).length on line 308 uses a
one-line body without curly braces, which violates the repository's style
guidelines requiring braces for all control statements. Add curly braces around
the return false statement to wrap the body. Apply the same fix to the similar
if statements also mentioned on lines 314 and 319.

Source: Coding guidelines

@nx-cloud

nx-cloudBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit d18d300

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded12m 49sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded2m 14sView ↗

☁️ Nx Cloud last updated this comment at 2026-06-22 11:10:10 UTC

@pkg-pr-new

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7664

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7664

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7664

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7664

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7664

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7664

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7664

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7664

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7664

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7664

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7664

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7664

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7664

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7664

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7664

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7664

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7664

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7664

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7664

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7664

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7664

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7664

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7664

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7664

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7664

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7664

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7664

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7664

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7664

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7664

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7664

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7664

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7664

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7664

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7664

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7664

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7664

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7664

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7664

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7664

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7664

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7664

commit: d18d300

@codspeed-hq

Copy link
Copy Markdown

Merging this PR will degrade performance by 11.21%

⚠️Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
❌ 5 regressed benchmarks
✅ 137 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

ModeBenchmarkBASEHEADEfficiency
Memorymem aborted-requests (solid)1.8 MB2.7 MB-35.01%
Memorymem serialization-payload (solid)6.9 MB8.9 MB-22.2%
Memorymem peak-large-page (solid)3.4 MB3.8 MB-11.32%
Memorymem navigation-churn (solid)1.4 MB1.5 MB-5.76%
Memorymem aborted-requests (vue)959.1 KB989.5 KB-3.07%
Memorymem streaming-peak chunked (vue)12.2 MB11.9 MB+3.1%
Memorymem request-churn (solid)1.2 MB1.1 MB+3.04%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing anonrig:perf/structural-sharing-enumerable-keys (d18d300) with main (f23ed0f)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (279a849) during the generation of this report, so f23ed0f was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Sheraff

Copy link
Copy Markdown
Collaborator

Replacement PR #8010 recreates this change on current main, preserves the original commit authorship, and addresses the outstanding block-body review.

Sign up for freeto 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.

2 participants

@anonrig@Sheraff
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

perf(router-core): faster enumerable-key collection in structural sharing - #7664

Closed
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/structural-sharing-enumerable-keys
Closed

perf(router-core): faster enumerable-key collection in structural sharing#7664
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/structural-sharing-enumerable-keys

Conversation

@anonrig

@anonriganonrig commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

What

Speed up getEnumerableOwnKeys — the helper that backs replaceEqualDeep (structural sharing) in packages/router-core/src/utils.ts.

It previously called Object.getOwnPropertyNames(o) and then invoked propertyIsEnumerableonce per key to confirm every own string property is enumerable. This swaps that O(n) loop of JS method calls for Object.keys(o) (native, enumerable-only) plus a single length comparison against getOwnPropertyNames(o) to detect non-enumerable own string props. Symbol handling is unchanged.

- const names = Object.getOwnPropertyNames(o)- for (const name of names) {- if (!isEnumerable.call(o, name)) return false- }+ const keys = Object.keys(o)+ if (keys.length !== Object.getOwnPropertyNames(o).length) return false

Why

replaceEqualDeep runs on every selector result on every state update when defaultStructuralSharing is enabled, so getEnumerableOwnKeys is called for every plain object it walks. For selector-heavy apps this is a hot client path (it short-circuits on the server, so this is purely a client-side win).

Benchmark on the real function (Vitest bench, node env where isServer is false), deeply-equal new state object shaped like typical router state (search / params / nested loaderData / context):

ops/s
before~560K
after~762K

1.36× faster in-situ (an isolated A/B of just the key-collection step measured ~1.55×). The win grows with object key count, since the old path made one propertyIsEnumerable call per key.

Correctness

Behavior is identical:

  • Same returned keys in the same order. Object.keys and Object.getOwnPropertyNames follow the same [[OwnPropertyKeys]] ordering (integer indices ascending, then string keys in insertion order), and in the non-bail case every string key is enumerable, so the two orderings coincide.
  • Still bails (returns false) for objects with any non-enumerable own string property (length mismatch) or any non-enumerable own symbol (existing per-symbol check).

Verified by:

  • The existing getEnumerableOwnKeys behavior (via replaceEqualDeep) suite (non-enumerable strings/symbols, frozen/sealed, Object.create(null), numeric keys, shadowing, mixed string+symbol).
  • A 5000+ case fuzz comparing the new key-collection against the old.
  • Two added tests: partial structural sharing on a nested object (unchanged subtrees keep their reference), and a nested object carrying a non-enumerable prop bailing to next.
nx run @tanstack/router-core:test:unit -> 1180 passed (+ 3 expected-fail)
nx run @tanstack/router-core:test:types -> no errors

Notes

  • Single-function change; replaceEqualDeep itself is untouched.
  • Includes a changeset (@tanstack/router-core patch).

Summary by CodeRabbit

  • Performance

    • Improved selector performance through optimized structural sharing computation in router core
  • Tests

    • Added test coverage for structural sharing behavior with nested objects and non-enumerable properties

…ring
getEnumerableOwnKeys (used by replaceEqualDeep) called
Object.getOwnPropertyNames and then invoked propertyIsEnumerable once per
key to verify every own string prop is enumerable. Replace that O(n) loop
of JS method calls with Object.keys (native, enumerable-only) plus a
single length comparison against getOwnPropertyNames to detect
non-enumerable string props. Symbol handling is unchanged.
Behavior is identical (verified against the existing suite + fuzzing): the
function still returns the enumerable own keys in the same order and still
bails (returns false) for objects with any non-enumerable own string or
symbol property.
replaceEqualDeep runs on every selector result on every state update when
defaultStructuralSharing is enabled, so this is a hot client path for
selector-heavy apps. Measured ~1.3-1.5x faster on typical router state.
@coderabbitai

coderabbitaiBot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

getEnumerableOwnKeys in utils.ts is refactored to detect non-enumerable string keys via an Object.keys vs Object.getOwnPropertyNames length comparison instead of iterating and calling propertyIsEnumerable per key. Two new tests for replaceEqualDeep are added, and a patch changeset entry is included.

Changes

Structural Sharing Enumerable Key Optimization

Layer / File(s)Summary
getEnumerableOwnKeys fast-path refactor and changeset
packages/router-core/src/utils.ts, .changeset/perf-structural-sharing-enumerable-keys.md
getEnumerableOwnKeys now compares Object.keys(obj).length against Object.getOwnPropertyNames(obj).length to detect non-enumerable string keys in one shot instead of looping with propertyIsEnumerable; symbol handling is retained. A patch changeset documents the optimization.
replaceEqualDeep identity-sharing and non-enumerable tests
packages/router-core/tests/utils.test.ts
Two new test cases added: one asserting unchanged nested subtree references are preserved when only a leaf value changes, and one asserting that a nested object with a non-enumerable property causes bail-out to the next reference while unchanged siblings retain identity.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • TanStack/router#7577: Refactors React hooks to centralize structural-sharing logic using the same replaceEqualDeep / useStructuralSharing implementation that this PR optimizes.

Poem

🐇 Hop, hop — no more looping through each key,
One length compare sets the fast path free!
Non-enumerable props? We bail with grace,
Identity preserved all over the place.
Object.keys wins this little race! 🗝️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately and concisely summarizes the main change: a performance optimization for enumerable-key collection in the structural sharing mechanism of router-core.
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/router-core/src/utils.ts`:
- Line 308: The if statement checking keys.length against
Object.getOwnPropertyNames(o).length on line 308 uses a one-line body without
curly braces, which violates the repository's style guidelines requiring braces
for all control statements. Add curly braces around the return false statement
to wrap the body. Apply the same fix to the similar if statements also mentioned
on lines 314 and 319.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c38a969f-acca-43e4-97f3-10cdc0a50a17

📥 Commits

Reviewing files that changed from the base of the PR and between 279a849 and d18d300.

📒 Files selected for processing (3)
  • .changeset/perf-structural-sharing-enumerable-keys.md
  • packages/router-core/src/utils.ts
  • packages/router-core/tests/utils.test.ts

// "clone-friendly" -> bail. This replaces an O(n) loop of
// `propertyIsEnumerable` calls with two native calls.
const keys = Object.keys(o)
if (keys.length !== Object.getOwnPropertyNames(o).length) return false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use block bodies for all changed if statements.

These one-line if bodies violate the repository rule requiring braces on control statements.

Suggested patch
- if (keys.length !== Object.getOwnPropertyNames(o).length) return false+ if (keys.length !== Object.getOwnPropertyNames(o).length) {+ return false+ }
@@
- if (symbols.length === 0) return keys+ if (symbols.length === 0) {+ return keys+ }
@@
- if (!isEnumerable.call(o, symbol)) return false+ if (!isEnumerable.call(o, symbol)) {+ return false+ }

As per coding guidelines, "**/*.{ts,tsx,js,jsx}: Always use curly braces for if, else, loops, and similar control statements."

Also applies to: 314-314, 319-319

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/router-core/src/utils.ts` at line 308, The if statement checking
keys.length against Object.getOwnPropertyNames(o).length on line 308 uses a
one-line body without curly braces, which violates the repository's style
guidelines requiring braces for all control statements. Add curly braces around
the return false statement to wrap the body. Apply the same fix to the similar
if statements also mentioned on lines 314 and 319.

Source: Coding guidelines

@nx-cloud

nx-cloudBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit d18d300

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded12m 49sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded2m 14sView ↗

☁️ Nx Cloud last updated this comment at 2026-06-22 11:10:10 UTC

@pkg-pr-new

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7664

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7664

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7664

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7664

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7664

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7664

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7664

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7664

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7664

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7664

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7664

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7664

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7664

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7664

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7664

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7664

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7664

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7664

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7664

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7664

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7664

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7664

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7664

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7664

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7664

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7664

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7664

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7664

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7664

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7664

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7664

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7664

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7664

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7664

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7664

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7664

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7664

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7664

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7664

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7664

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7664

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7664

commit: d18d300

@codspeed-hq

Copy link
Copy Markdown

Merging this PR will degrade performance by 11.21%

⚠️Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
❌ 5 regressed benchmarks
✅ 137 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

ModeBenchmarkBASEHEADEfficiency
Memorymem aborted-requests (solid)1.8 MB2.7 MB-35.01%
Memorymem serialization-payload (solid)6.9 MB8.9 MB-22.2%
Memorymem peak-large-page (solid)3.4 MB3.8 MB-11.32%
Memorymem navigation-churn (solid)1.4 MB1.5 MB-5.76%
Memorymem aborted-requests (vue)959.1 KB989.5 KB-3.07%
Memorymem streaming-peak chunked (vue)12.2 MB11.9 MB+3.1%
Memorymem request-churn (solid)1.2 MB1.1 MB+3.04%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing anonrig:perf/structural-sharing-enumerable-keys (d18d300) with main (f23ed0f)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (279a849) during the generation of this report, so f23ed0f was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Sheraff

Copy link
Copy Markdown
Collaborator

Replacement PR #8010 recreates this change on current main, preserves the original commit authorship, and addresses the outstanding block-body review.

Sign up for freeto 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.

2 participants

@anonrig@Sheraff
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

perf(router-core): faster enumerable-key collection in structural sharing - #7664

Closed
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/structural-sharing-enumerable-keys
Closed

perf(router-core): faster enumerable-key collection in structural sharing#7664
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/structural-sharing-enumerable-keys

Conversation

@anonrig

@anonriganonrig commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

What

Speed up getEnumerableOwnKeys — the helper that backs replaceEqualDeep (structural sharing) in packages/router-core/src/utils.ts.

It previously called Object.getOwnPropertyNames(o) and then invoked propertyIsEnumerableonce per key to confirm every own string property is enumerable. This swaps that O(n) loop of JS method calls for Object.keys(o) (native, enumerable-only) plus a single length comparison against getOwnPropertyNames(o) to detect non-enumerable own string props. Symbol handling is unchanged.

- const names = Object.getOwnPropertyNames(o)- for (const name of names) {- if (!isEnumerable.call(o, name)) return false- }+ const keys = Object.keys(o)+ if (keys.length !== Object.getOwnPropertyNames(o).length) return false

Why

replaceEqualDeep runs on every selector result on every state update when defaultStructuralSharing is enabled, so getEnumerableOwnKeys is called for every plain object it walks. For selector-heavy apps this is a hot client path (it short-circuits on the server, so this is purely a client-side win).

Benchmark on the real function (Vitest bench, node env where isServer is false), deeply-equal new state object shaped like typical router state (search / params / nested loaderData / context):

ops/s
before~560K
after~762K

1.36× faster in-situ (an isolated A/B of just the key-collection step measured ~1.55×). The win grows with object key count, since the old path made one propertyIsEnumerable call per key.

Correctness

Behavior is identical:

  • Same returned keys in the same order. Object.keys and Object.getOwnPropertyNames follow the same [[OwnPropertyKeys]] ordering (integer indices ascending, then string keys in insertion order), and in the non-bail case every string key is enumerable, so the two orderings coincide.
  • Still bails (returns false) for objects with any non-enumerable own string property (length mismatch) or any non-enumerable own symbol (existing per-symbol check).

Verified by:

  • The existing getEnumerableOwnKeys behavior (via replaceEqualDeep) suite (non-enumerable strings/symbols, frozen/sealed, Object.create(null), numeric keys, shadowing, mixed string+symbol).
  • A 5000+ case fuzz comparing the new key-collection against the old.
  • Two added tests: partial structural sharing on a nested object (unchanged subtrees keep their reference), and a nested object carrying a non-enumerable prop bailing to next.
nx run @tanstack/router-core:test:unit -> 1180 passed (+ 3 expected-fail)
nx run @tanstack/router-core:test:types -> no errors

Notes

  • Single-function change; replaceEqualDeep itself is untouched.
  • Includes a changeset (@tanstack/router-core patch).

Summary by CodeRabbit

  • Performance

    • Improved selector performance through optimized structural sharing computation in router core
  • Tests

    • Added test coverage for structural sharing behavior with nested objects and non-enumerable properties

…ring
getEnumerableOwnKeys (used by replaceEqualDeep) called
Object.getOwnPropertyNames and then invoked propertyIsEnumerable once per
key to verify every own string prop is enumerable. Replace that O(n) loop
of JS method calls with Object.keys (native, enumerable-only) plus a
single length comparison against getOwnPropertyNames to detect
non-enumerable string props. Symbol handling is unchanged.
Behavior is identical (verified against the existing suite + fuzzing): the
function still returns the enumerable own keys in the same order and still
bails (returns false) for objects with any non-enumerable own string or
symbol property.
replaceEqualDeep runs on every selector result on every state update when
defaultStructuralSharing is enabled, so this is a hot client path for
selector-heavy apps. Measured ~1.3-1.5x faster on typical router state.
@coderabbitai

coderabbitaiBot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

getEnumerableOwnKeys in utils.ts is refactored to detect non-enumerable string keys via an Object.keys vs Object.getOwnPropertyNames length comparison instead of iterating and calling propertyIsEnumerable per key. Two new tests for replaceEqualDeep are added, and a patch changeset entry is included.

Changes

Structural Sharing Enumerable Key Optimization

Layer / File(s)Summary
getEnumerableOwnKeys fast-path refactor and changeset
packages/router-core/src/utils.ts, .changeset/perf-structural-sharing-enumerable-keys.md
getEnumerableOwnKeys now compares Object.keys(obj).length against Object.getOwnPropertyNames(obj).length to detect non-enumerable string keys in one shot instead of looping with propertyIsEnumerable; symbol handling is retained. A patch changeset documents the optimization.
replaceEqualDeep identity-sharing and non-enumerable tests
packages/router-core/tests/utils.test.ts
Two new test cases added: one asserting unchanged nested subtree references are preserved when only a leaf value changes, and one asserting that a nested object with a non-enumerable property causes bail-out to the next reference while unchanged siblings retain identity.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • TanStack/router#7577: Refactors React hooks to centralize structural-sharing logic using the same replaceEqualDeep / useStructuralSharing implementation that this PR optimizes.

Poem

🐇 Hop, hop — no more looping through each key,
One length compare sets the fast path free!
Non-enumerable props? We bail with grace,
Identity preserved all over the place.
Object.keys wins this little race! 🗝️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately and concisely summarizes the main change: a performance optimization for enumerable-key collection in the structural sharing mechanism of router-core.
Docstring Coverage✅ PassedDocstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/router-core/src/utils.ts`:
- Line 308: The if statement checking keys.length against
Object.getOwnPropertyNames(o).length on line 308 uses a one-line body without
curly braces, which violates the repository's style guidelines requiring braces
for all control statements. Add curly braces around the return false statement
to wrap the body. Apply the same fix to the similar if statements also mentioned
on lines 314 and 319.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c38a969f-acca-43e4-97f3-10cdc0a50a17

📥 Commits

Reviewing files that changed from the base of the PR and between 279a849 and d18d300.

📒 Files selected for processing (3)
  • .changeset/perf-structural-sharing-enumerable-keys.md
  • packages/router-core/src/utils.ts
  • packages/router-core/tests/utils.test.ts

// "clone-friendly" -> bail. This replaces an O(n) loop of
// `propertyIsEnumerable` calls with two native calls.
const keys = Object.keys(o)
if (keys.length !== Object.getOwnPropertyNames(o).length) return false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use block bodies for all changed if statements.

These one-line if bodies violate the repository rule requiring braces on control statements.

Suggested patch
- if (keys.length !== Object.getOwnPropertyNames(o).length) return false+ if (keys.length !== Object.getOwnPropertyNames(o).length) {+ return false+ }
@@
- if (symbols.length === 0) return keys+ if (symbols.length === 0) {+ return keys+ }
@@
- if (!isEnumerable.call(o, symbol)) return false+ if (!isEnumerable.call(o, symbol)) {+ return false+ }

As per coding guidelines, "**/*.{ts,tsx,js,jsx}: Always use curly braces for if, else, loops, and similar control statements."

Also applies to: 314-314, 319-319

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/router-core/src/utils.ts` at line 308, The if statement checking
keys.length against Object.getOwnPropertyNames(o).length on line 308 uses a
one-line body without curly braces, which violates the repository's style
guidelines requiring braces for all control statements. Add curly braces around
the return false statement to wrap the body. Apply the same fix to the similar
if statements also mentioned on lines 314 and 319.

Source: Coding guidelines

@nx-cloud

nx-cloudBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit d18d300

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded12m 49sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded2m 14sView ↗

☁️ Nx Cloud last updated this comment at 2026-06-22 11:10:10 UTC

@pkg-pr-new

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7664

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7664

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7664

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7664

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7664

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7664

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7664

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7664

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7664

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7664

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7664

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7664

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7664

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7664

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7664

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7664

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7664

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7664

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7664

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7664

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7664

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7664

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7664

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7664

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7664

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7664

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7664

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7664

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7664

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7664

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7664

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7664

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7664

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7664

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7664

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7664

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7664

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7664

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7664

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7664

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7664

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7664

commit: d18d300

@codspeed-hq

Copy link
Copy Markdown

Merging this PR will degrade performance by 11.21%

⚠️Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
❌ 5 regressed benchmarks
✅ 137 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

ModeBenchmarkBASEHEADEfficiency
Memorymem aborted-requests (solid)1.8 MB2.7 MB-35.01%
Memorymem serialization-payload (solid)6.9 MB8.9 MB-22.2%
Memorymem peak-large-page (solid)3.4 MB3.8 MB-11.32%
Memorymem navigation-churn (solid)1.4 MB1.5 MB-5.76%
Memorymem aborted-requests (vue)959.1 KB989.5 KB-3.07%
Memorymem streaming-peak chunked (vue)12.2 MB11.9 MB+3.1%
Memorymem request-churn (solid)1.2 MB1.1 MB+3.04%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing anonrig:perf/structural-sharing-enumerable-keys (d18d300) with main (f23ed0f)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (279a849) during the generation of this report, so f23ed0f was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Sheraff

Copy link
Copy Markdown
Collaborator

Replacement PR #8010 recreates this change on current main, preserves the original commit authorship, and addresses the outstanding block-body review.

Sign up for freeto 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.

2 participants

@anonrig@Sheraff