Skip to content

Conversation

Sheraff
Copy link
Contributor

@Sheraff Sheraff commented Aug 29, 2025

Description

Improve performance of this function that gets called a lot for structural sharing. Main improvements are

  • avoid creating object when possible (Object.keys().concat(Object.getOwnPropertySymbols()) creates 3 arrays, when we only want 1)
  • instantiating array to the correct size avoids a lot of memory management under the hood (prefer new Array(size) over [])
  • avoid reading the same value many times on an object, store is as a const

More minor changes (not 100% sure I can measure it, but I think so):

  • using keys.includes(k) is slower than Object.hasOwnProperty.call(obj, k) or obj.hasOwnProperty(k)

benchmark

TL;DR: consistently 1.3x faster implementation

describe('replaceEqualDeep', () => {
  bench('old implementation', () => {
    replaceEqualDeepOld({ a: 1, b: [2, 3] }, { a: 1, b: [2, 3] })
    replaceEqualDeepOld({ a: 1, b: [2, 3] }, { a: 2, b: [2] })
  })

  bench('new implementation', () => {
    replaceEqualDeep({ a: 1, b: [2, 3] }, { a: 1, b: [2, 3] })
    replaceEqualDeep({ a: 1, b: [2, 3] }, { a: 2, b: [2] })
  })
})
 ✓  @tanstack/router-core  tests/utils.bench.ts > replaceEqualDeep 1540ms
     name                          hz     min     max    mean     p75     p99    p995    p999     rme  samples
   · old implementation  1,040,201.62  0.0008  0.7638  0.0010  0.0010  0.0013  0.0016  0.0022  ±0.33%   520101
   · new implementation  1,347,988.70  0.0006  2.4037  0.0007  0.0007  0.0010  0.0010  0.0013  ±0.95%   673995   fastest

 BENCH  Summary

   @tanstack/router-core  new implementation - tests/utils.bench.ts > replaceEqualDeep
    1.30x faster than old implementation

The replaceEqualDeep implementation before this PR handles Symbol keys, and non-enumerable keys (see #4237), but not keys that are both a Symbol and non-enumerable.

This PR fixes this issue. But if we also fix it in the previous implementation before comparing performance we get a bigger perf diff

 ✓  @tanstack/router-core  tests/utils.bench.ts > replaceEqualDeep 1471ms
     name                          hz     min     max    mean     p75     p99    p995    p999     rme  samples
   · old implementation    713,964.88  0.0012  0.7880  0.0014  0.0014  0.0019  0.0023  0.0050  ±0.35%   356983
   · new implementation  1,319,003.07  0.0006  5.0000  0.0008  0.0007  0.0010  0.0016  0.0050  ±1.96%   659502   fastest

 BENCH  Summary

   @tanstack/router-core  new implementation - tests/utils.bench.ts > replaceEqualDeep
    1.85x faster than old implementation

Summary by CodeRabbit

  • Bug Fixes

    • Change detection now includes symbol-keyed properties, treats undefined/missing entries consistently, and avoids processing objects with non-enumerable own properties to prevent incorrect updates.
    • No public API changes.
  • Performance

    • Equality/merge logic reuses unchanged values more reliably and improves array update handling by allocating appropriately, reducing unnecessary work.

Copy link

coderabbitai bot commented Aug 29, 2025

Walkthrough

Introduces enumerable-own-key checks and an early bailout for non-object/non-array inputs, replaces Reflect.ownKeys-based enumeration with a getEnumerableOwnKeys helper that bails on non-enumerable own properties, pre-sizes array copies, and caches p = prev[key] while simplifying per-key deep-replace logic. (≤50 words)

Changes

Cohort / File(s) Summary
Router Core Utilities
packages/router-core/src/utils.ts
Add getEnumerableOwnKeys(o) to return enumerable own keys or false if any own property is non-enumerable; early exit when input is neither array nor plain object; use enumerable-own-keys for object iteration; pre-size arrays with new Array(nextSize); introduce local cache p = prev[key] and single value = replaceEqualDeep(p, next[key]) flow; adjust existence/undefined checks and equality counting; preserve public API signatures.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant Utils as utils.replaceEqualDeep
  Caller->>Utils: replaceEqualDeep(prev, next)
  activate Utils
  Utils->>Utils: determine types (array / plain object / other)
  alt non-array & non-object
    Utils-->>Caller: return next (early bailout)
  else array
    Utils->>Utils: allocate copy = new Array(nextSize)
    loop indices
      Utils->>Utils: compare prev[i] and next[i] (deep)
      Utils->>Utils: assign reuse or copy[i] = value
    end
  else object
    Utils->>Utils: prevKeys = getEnumerableOwnKeys(prev)
    Utils->>Utils: nextKeys = getEnumerableOwnKeys(next)
    alt any returned false
      Utils-->>Caller: return next (non-enumerable own prop found)
    else
      Utils->>Utils: keys = union(prevKeys, nextKeys)
      loop for each key
        Utils->>Utils: p = prev[key]  %% local cache
        Utils->>Utils: if (array || prev.hasOwnProperty(key))? 
        alt prev missing/undefined
          Utils->>Utils: copy[key] = next[key]
        else
          Utils->>Utils: value = replaceEqualDeep(p, next[key])
          Utils->>Utils: if value === p && p !== undefined then reuse p else copy[key] = value
        end
      end
      Utils-->>Caller: return prev if sizes and equalItems match else return copy
    end
  end
  deactivate Utils
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • schiller-manuel
  • SeanCassiere

Poem

I sniffed the keys where shadows play,
Found only gems that stayed all day.
Cached a carrot—p—and hopped along,
Pre-sized arrays and a single song.
Reused what's true; the merge is strong. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-router-core-replace-equal-deep-perf

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

nx-cloud bot commented Aug 29, 2025

View your CI Pipeline Execution ↗ for commit 4b5515d

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 4m 23s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 32s View ↗

☁️ Nx Cloud last updated this comment at 2025-08-30 11:08:38 UTC

Copy link

pkg-pr-new bot commented Aug 29, 2025

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@5046

@tanstack/directive-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/directive-functions-plugin@5046

@tanstack/eslint-plugin-router

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

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@5046

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@5046

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@5046

@tanstack/react-start-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-plugin@5046

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@5046

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/server-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/server-functions-plugin@5046

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@5046

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@5046

@tanstack/solid-start-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-plugin@5046

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@5046

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@5046

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@5046

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@5046

@tanstack/start-server-functions-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-client@5046

@tanstack/start-server-functions-fetcher

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-fetcher@5046

@tanstack/start-server-functions-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-server@5046

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@5046

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@5046

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@5046

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@5046

commit: 4b5515d

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/router-core/src/utils.ts (1)

231-235: Prefer hasOwnProperty over includes for O(1) membership

Avoids an O(n) includes scan per key when walking objects; short-circuits unchanged for arrays.

Apply this diff:

-      if (
-        (array || prevItems.includes(key)) &&
+      if (
+        (array || Object.prototype.hasOwnProperty.call(prev, key)) &&
         prev[key] === undefined &&
         next[key] === undefined
       ) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 90e9006 and e4f4a9f.

📒 Files selected for processing (1)
  • packages/router-core/src/utils.ts (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (1)
packages/router-core/src/utils.ts (1)

222-226: LGTM: Switch to Reflect.ownKeys reduces allocations without changing semantics

Given isSimplePlainObject guards out non-enumerable string keys, this preserves behavior while likely shaving work vs keys+symbols concat.

Consider adding a tiny assertion-based test that covers symbol keys and undefined-valued own props to confirm parity with the old impl.

@Sheraff Sheraff changed the title refactor(router-core): very slightly faster replaceEqualDeep refactor(router-core): slightly faster replaceEqualDeep Aug 30, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/router-core/src/utils.ts (2)

220-223: Rename the object flag to avoid confusion with the object type.

Minor naming nit to improve readability.

-  const object = !array && isPlainObject(prev) && isPlainObject(next)
+  const plainObject = !array && isPlainObject(prev) && isPlainObject(next)

-  if (!array && !object) return next
+  if (!array && !plainObject) return next

257-273: Type the helper and preallocate keys to avoid push growth.

Matches the PR’s “preallocate arrays” rationale and tightens types for callers.

-function getEnumerableOwnKeys(o: object) {
-  const keys = []
-  const names = Object.getOwnPropertyNames(o)
-  for (const name of names) {
-    if (!Object.prototype.propertyIsEnumerable.call(o, name)) return false
-    keys.push(name)
-  }
-  const symbols = Object.getOwnPropertySymbols(o)
-  for (const symbol of symbols) {
-    if (!Object.prototype.propertyIsEnumerable.call(o, symbol)) return false
-    keys.push(symbol)
-  }
-  return keys
+function getEnumerableOwnKeys(
+  o: object,
+): Array<string | symbol> | false {
+  const names = Object.getOwnPropertyNames(o)
+  const symbols = Object.getOwnPropertySymbols(o)
+  const keys: Array<string | symbol> = new Array(
+    names.length + symbols.length,
+  )
+  let i = 0
+  for (let j = 0; j < names.length; j++) {
+    const name = names[j]!
+    if (!Object.prototype.propertyIsEnumerable.call(o, name)) return false
+    keys[i++] = name
+  }
+  for (let j = 0; j < symbols.length; j++) {
+    const symbol = symbols[j]!
+    if (!Object.prototype.propertyIsEnumerable.call(o, symbol)) return false
+    keys[i++] = symbol
+  }
+  return keys
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bb5fe97 and 4b5515d.

📒 Files selected for processing (1)
  • packages/router-core/src/utils.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: Sheraff
PR: TanStack/router#5051
File: packages/router-core/src/utils.ts:310-315
Timestamp: 2025-08-30T09:12:13.841Z
Learning: In TanStack Router's deepEqual utility, using for..in instead of Object.keys() in getObjectKeys() when ignoreUndefined=true is acceptable because it's called only after isPlainObject() checks, which ensure objects have standard Object prototype chains with no inherited enumerable properties.
📚 Learning: 2025-08-30T09:12:13.841Z
Learnt from: Sheraff
PR: TanStack/router#5051
File: packages/router-core/src/utils.ts:310-315
Timestamp: 2025-08-30T09:12:13.841Z
Learning: In TanStack Router's deepEqual utility, using for..in instead of Object.keys() in getObjectKeys() when ignoreUndefined=true is acceptable because it's called only after isPlainObject() checks, which ensure objects have standard Object prototype chains with no inherited enumerable properties.

Applied to files:

  • packages/router-core/src/utils.ts
🧬 Code graph analysis (1)
packages/router-core/src/utils.ts (3)
packages/react-router/src/index.tsx (2)
  • isPlainObject (28-28)
  • replaceEqualDeep (27-27)
packages/solid-router/src/index.tsx (2)
  • isPlainObject (28-28)
  • replaceEqualDeep (27-27)
packages/router-core/src/index.ts (2)
  • isPlainObject (273-273)
  • replaceEqualDeep (272-272)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test
🔇 Additional comments (1)
packages/router-core/src/utils.ts (1)

224-231: LGTM: key collection + array pre-sizing.

The enumerable-own-keys bailout and array preallocation align with the perf goal.

Comment on lines +234 to 254
for (let i = 0; i < nextSize; i++) {
const key = array ? i : (nextItems[i] as any)
const p = prev[key]
if (
(array || prev.hasOwnProperty(key)) &&
p === undefined &&
next[key] === undefined
) {
copy[key] = undefined
equalItems++
} else {
const value = replaceEqualDeep(p, next[key])
copy[key] = value
if (value === p && p !== undefined) {
equalItems++
} else {
copy[key] = replaceEqualDeep(prev[key], next[key])
if (copy[key] === prev[key] && prev[key] !== undefined) {
equalItems++
}
}
}

return prevSize === nextSize && equalItems === prevSize ? prev : copy
}

return next
return prevSize === nextSize && equalItems === prevSize ? prev : copy
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use a safe hasOwnProperty check; current call can throw if shadowed. Also cache next[key].

Objects can define a string key named "hasOwnProperty", which would make prev.hasOwnProperty non-callable and throw at runtime. Prefer Object.prototype.hasOwnProperty.call. Caching next[key] avoids repeated lookups.

-    const key = array ? i : (nextItems[i] as any)
-    const p = prev[key]
-    if (
-      (array || prev.hasOwnProperty(key)) &&
-      p === undefined &&
-      next[key] === undefined
-    ) {
+    const key = array ? i : (nextItems[i] as any)
+    const p = prev[key]
+    const n = next[key]
+    const hasPrevKey = array || Object.prototype.hasOwnProperty.call(prev, key)
+    if (hasPrevKey && p === undefined && n === undefined) {
       copy[key] = undefined
       equalItems++
     } else {
-      const value = replaceEqualDeep(p, next[key])
+      const value = replaceEqualDeep(p, n)
       copy[key] = value
       if (value === p && p !== undefined) {
         equalItems++
       }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (let i = 0; i < nextSize; i++) {
const key = array ? i : (nextItems[i] as any)
const p = prev[key]
if (
(array || prev.hasOwnProperty(key)) &&
p === undefined &&
next[key] === undefined
) {
copy[key] = undefined
equalItems++
} else {
const value = replaceEqualDeep(p, next[key])
copy[key] = value
if (value === p && p !== undefined) {
equalItems++
} else {
copy[key] = replaceEqualDeep(prev[key], next[key])
if (copy[key] === prev[key] && prev[key] !== undefined) {
equalItems++
}
}
}
return prevSize === nextSize && equalItems === prevSize ? prev : copy
}
return next
return prevSize === nextSize && equalItems === prevSize ? prev : copy
}
for (let i = 0; i < nextSize; i++) {
const key = array ? i : (nextItems[i] as any)
const p = prev[key]
const n = next[key]
const hasPrevKey = array || Object.prototype.hasOwnProperty.call(prev, key)
if (hasPrevKey && p === undefined && n === undefined) {
copy[key] = undefined
equalItems++
} else {
const value = replaceEqualDeep(p, n)
copy[key] = value
if (value === p && p !== undefined) {
equalItems++
}
}
}
return prevSize === nextSize && equalItems === prevSize ? prev : copy
}
🤖 Prompt for AI Agents
In packages/router-core/src/utils.ts around lines 234 to 254, the loop uses
prev.hasOwnProperty(key) which can throw if an object shadowed that method and
it repeatedly reads next[key]; change to use
Object.prototype.hasOwnProperty.call(prev, key) and cache next[key] into a local
variable (e.g. const nextVal = next[key]) before using it so you reuse nextVal
for the undefined checks and when calling replaceEqualDeep, replacing all
occurrences of next[key] with the cached nextVal.

@Sheraff Sheraff merged commit eb6bcea into main Aug 30, 2025
6 checks passed
@Sheraff Sheraff deleted the refactor-router-core-replace-equal-deep-perf branch August 30, 2025 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants