-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
refactor(query-core): improve replaceEqualDeep performance #9604
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor(query-core): improve replaceEqualDeep performance #9604
Conversation
WalkthroughReplaces previous replaceEqualDeep with a unified deep-merge handling arrays and plain objects, adds type guards ( Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Caller
participant Utils as replaceEqualDeep
Caller->>Utils: replaceEqualDeep(a, b)
alt not both arrays nor both plain objects
Utils-->>Caller: return b
else Arrays
Note over Utils: build copy = new Array(b.length)
loop for each index i
Utils->>Utils: aItem = a[i]\nrecurse = replaceEqualDeep(aItem, b[i])
alt aItem === b[i] or both undefined
Note over Utils: equalItems++
else
Note over Utils: copy[i] = recurse
end
end
alt lengths match and equalItems == a.length
Utils-->>Caller: return a
else
Utils-->>Caller: return copy
end
else Plain Objects
Note over Utils: build copy = {}
loop for each own key in b
Utils->>Utils: aItem = a[key]\nrecurse = replaceEqualDeep(aItem, b[key])
alt aItem === b[key] or both undefined
Note over Utils: equalItems++
else
Note over Utils: copy[key] = recurse
end
end
alt own-key counts match and equalItems == aKeyCount
Utils-->>Caller: return a
else
Utils-->>Caller: return copy
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
🤖 Nx Cloud AI Fix Eligible
To disable these notifications, a workspace admin can disable them in workspace settings. View your CI Pipeline Execution ↗ for commit 91494ca
☁️ Nx Cloud last updated this comment at |
More templates
- @tanstack/query-example-angular-auto-refetching
- @tanstack/query-example-angular-basic
- @tanstack/query-example-angular-basic-persister
- @tanstack/query-example-angular-devtools-panel
- @tanstack/query-example-angular-infinite-query-with-max-pages
- @tanstack/query-example-angular-optimistic-updates
- @tanstack/query-example-angular-pagination
- @tanstack/query-example-angular-query-options-from-a-service
- @tanstack/query-example-angular-router
- @tanstack/query-example-angular-rxjs
- @tanstack/query-example-angular-simple
- @tanstack/query-example-react-algolia
- @tanstack/query-example-react-auto-refetching
- @tanstack/query-example-react-basic
- @tanstack/query-example-react-basic-graphql-request
- @tanstack/query-example-chat
- @tanstack/query-example-react-default-query-function
- @tanstack/query-example-react-devtools-panel
- @tanstack/query-example-eslint-legacy
- @tanstack/query-example-react-infinite-query-with-max-pages
- @tanstack/query-example-react-load-more-infinite-scroll
- @tanstack/query-example-react-nextjs
- @tanstack/query-example-react-nextjs-app-prefetching
- @tanstack/query-example-nextjs-suspense-streaming
- @tanstack/query-example-react-offline
- @tanstack/query-example-react-optimistic-updates-cache
- @tanstack/query-example-react-optimistic-updates-ui
- @tanstack/query-example-react-pagination
- @tanstack/query-example-react-playground
- @tanstack/query-example-react-prefetching
- @tanstack/query-example-react-react-native
- @tanstack/query-example-react-router
- @tanstack/query-example-react-rick-morty
- @tanstack/query-example-react-shadow-dom
- @tanstack/query-example-react-simple
- @tanstack/query-example-react-star-wars
- @tanstack/query-example-react-suspense
- @tanstack/query-example-solid-astro
- @tanstack/query-example-solid-basic
- @tanstack/query-example-solid-basic-graphql-request
- @tanstack/query-example-solid-default-query-function
- @tanstack/query-example-solid-simple
- @tanstack/query-example-solid-start-streaming
- @tanstack/query-example-svelte-auto-refetching
- @tanstack/query-example-svelte-basic
- @tanstack/query-example-svelte-load-more-infinite-scroll
- @tanstack/query-example-svelte-optimistic-updates
- @tanstack/query-example-svelte-playground
- @tanstack/query-example-svelte-simple
- @tanstack/query-example-svelte-ssr
- @tanstack/query-example-svelte-star-wars
- @tanstack/query-example-vue-2.6-basic
- @tanstack/query-example-vue-2.7-basic
- @tanstack/query-example-vue-basic
- @tanstack/query-example-vue-dependent-queries
- @tanstack/query-example-vue-nuxt3
- @tanstack/query-example-vue-persister
- @tanstack/query-example-vue-simple
@tanstack/angular-query-devtools-experimental
npm i https://pkg.pr.new/@tanstack/angular-query-devtools-experimental@9604
@tanstack/angular-query-experimental
npm i https://pkg.pr.new/@tanstack/angular-query-experimental@9604
@tanstack/eslint-plugin-query
npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@9604
@tanstack/query-async-storage-persister
npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@9604
@tanstack/query-broadcast-client-experimental
npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@9604
@tanstack/query-core
npm i https://pkg.pr.new/@tanstack/query-core@9604
@tanstack/query-devtools
npm i https://pkg.pr.new/@tanstack/query-devtools@9604
@tanstack/query-persist-client-core
npm i https://pkg.pr.new/@tanstack/query-persist-client-core@9604
@tanstack/query-sync-storage-persister
npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@9604
@tanstack/react-query
npm i https://pkg.pr.new/@tanstack/react-query@9604
@tanstack/react-query-devtools
npm i https://pkg.pr.new/@tanstack/react-query-devtools@9604
@tanstack/react-query-next-experimental
npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@9604
@tanstack/react-query-persist-client
npm i https://pkg.pr.new/@tanstack/react-query-persist-client@9604
@tanstack/solid-query
npm i https://pkg.pr.new/@tanstack/solid-query@9604
@tanstack/solid-query-devtools
npm i https://pkg.pr.new/@tanstack/solid-query-devtools@9604
@tanstack/solid-query-persist-client
npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@9604
@tanstack/svelte-query
npm i https://pkg.pr.new/@tanstack/svelte-query@9604
@tanstack/svelte-query-devtools
npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@9604
@tanstack/svelte-query-persist-client
npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@9604
@tanstack/vue-query
npm i https://pkg.pr.new/@tanstack/vue-query@9604
@tanstack/vue-query-devtools
npm i https://pkg.pr.new/@tanstack/vue-query-devtools@9604
commit: 91494ca
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@ ## main #9604 +/- ## =========================================== + Coverage 45.17% 59.48% +14.30% =========================================== Files 208 137 -71 Lines 8327 5595 -2732 Branches 1878 1500 -378 =========================================== - Hits 3762 3328 -434 + Misses 4118 1963 -2155 + Partials 447 304 -143 🚀 New features to boost your workflow:
|
@coderabbitai
coderabbitai
bot
left a comment
There was a problem hiding this 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 (1)
packages/query-core/src/utils.ts (1)
273-286
: Micro-opt: hoist hasOwn and avoid double reads of b[key]Hoist
hasOwn
and reusebItem
to shave a couple of lookups in tight loops.Apply:
- let equalItems = 0 + const hasOwn = Object.prototype.hasOwnProperty + let equalItems = 0 @@ - if ( - (array || Object.prototype.hasOwnProperty.call(a, key)) && + if ( + (array || hasOwn.call(a, key)) &&
📜 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.
📒 Files selected for processing (1)
packages/query-core/src/utils.ts
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/query-core/src/utils.ts (1)
packages/query-core/src/index.ts (1)
replaceEqualDeep
(19-19)
⏰ 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 (3)
packages/query-core/src/utils.ts (3)
262-263
: Confirm intent: bail out for non-plain arrays/objectsEarly-returning
b
when neither a plain array nor a plain object skips structural sharing for sparse arrays, typed arrays, class instances, Dates, Maps/Sets, etc. Verify this matches prior behavior and expectations for consumers. If parity with previous semantics is desired, add tests around sparse arrays and typed arrays.
264-269
: Prealloc and key-caching look goodPre-allocating
new Array(bSize)
and caching key lists should reduce allocations and property lookups on hot paths.
292-292
: Return condition reads clearlyReusing
a
only when sizes match and all items are equal preserves structural sharing without false positives. LGTM.
@coderabbitai
coderabbitai
bot
left a comment
There was a problem hiding this 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
♻️ Duplicate comments (1)
packages/query-core/src/utils.ts (1)
271-283
: Cache bItem to avoid repeated property lookups inside the hot loop.Reads
b[key]
twice per iteration. Caching improves locality and aligns with the PR’s stated perf goals.for (let i = 0; i < bSize; i++) { const key = array ? i : bItems[i] const aItem = a[key] + const bItem = b[key] if ( - (array || Object.prototype.hasOwnProperty.call(a, key)) && - aItem === undefined && - b[key] === undefined + (array || Object.prototype.hasOwnProperty.call(a, key)) && + aItem === undefined && + bItem === undefined ) { copy[key] = undefined equalItems++ } else { - const value = replaceEqualDeep(aItem, b[key]) + const value = replaceEqualDeep(aItem, bItem) copy[key] = value if (value === aItem && aItem !== undefined) { equalItems++ } } }
🧹 Nitpick comments (1)
packages/query-core/src/utils.ts (1)
261-268
: Early-return and pre-allocation look good; trim an unnecessary allocation.
aItems
is only used to computeaSize
. Avoid creating it to reduce work/memory.- const aItems = array ? a : Object.keys(a) - const aSize = aItems.length + const aSize = array ? a.length : Object.keys(a).length
📜 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.
📒 Files selected for processing (1)
packages/query-core/src/utils.ts
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/query-core/src/utils.ts (1)
packages/query-core/src/index.ts (1)
replaceEqualDeep
(19-19)
⏰ 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/query-core/src/utils.ts (1)
275-276
: Prototype-safe hasOwnProperty: LGTM.Switching to
Object.prototype.hasOwnProperty.call(a, key)
avoids issues whenhasOwnProperty
is shadowed.
@coderabbitai
coderabbitai
bot
left a comment
There was a problem hiding this 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
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/query-core/src/utils.ts (1)
373-402
: Use safe own-check and correct prototype comment
- Change the own-property check on
prot
to use the helper for safety:if (!hasOwn.call(prot, 'isPrototypeOf')) { return false }- Revise the comment before the
getPrototypeOf
guard to accurately state that we reject any object whose prototype isn’t exactlyObject.prototype
(null-proto objects are already accepted by the earlyctor === undefined
return).
♻️ Duplicate comments (1)
packages/query-core/src/utils.ts (1)
248-248
: Good fix: prototype-safe own-property check and cached reads.Resolved the earlier "shadowed hasOwnProperty" concern and reduced repeated property access by caching
aItem
/bItem
. Looks solid.Also applies to: 319-327
🧹 Nitpick comments (3)
packages/query-core/src/utils.ts (3)
255-256
: Preferunknown
overany
in the implementation signature.Keeping the overload
replaceEqualDeep<T>(a: unknown, b: T): T
is great. For the implementation, returningunknown
avoids leakingany
while staying compatible with the overloads.-export function replaceEqualDeep(a: unknown, b: unknown): any { +export function replaceEqualDeep(a: unknown, b: unknown): unknown {
311-345
: Object path: tight loop and O(1) own-checks LGTM.
for..in
overb
avoids materializing key arrays.- Equality counting gated by
hasOwn.call(a, k)
is correct.- Recursion preserves references where possible.
One note: symbol keys aren’t considered (consistent with JSON-only constraint). If that’s intentional, consider documenting it near
replaceData
to preempt surprises.
368-370
: Sparse/augmented arrays won’t be treated as "plain arrays". Please confirm.
isPlainArray
returns false for sparse arrays or arrays with extra enumerable props (length !== Object.keys(value).length
). That changes behavior vs a naiveArray.isArray
check and will fall back to "return b" instead of structural sharing. If intentional, add tests for:
- sparse arrays (holes)
- arrays with extra enumerable props
I can draft these tests if desired.
📜 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.
📒 Files selected for processing (1)
packages/query-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). (1)
- GitHub Check: Test
🔇 Additional comments (2)
packages/query-core/src/utils.ts (2)
261-299
: Array path perf refactor looks correct.Pre-allocation + strict-equality fast path + recursive sharing are implemented correctly, and the equality accounting (
i < aSize
) handles length mismatches safely.
301-309
: Type-mismatch early return preserves previous semantics.Returning
b
when only one side is an array or a non-plain object matches the intended "JSON-only" structural sharing behavior. Good.
packages/query-core/src/utils.ts
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could the "only 1 is an array" case not be removed because we would also go into the "at least 1 is not an object" case and also return b
there?
all tests still pass if I just remove that check, and we could then also inline aIsArr
and bIsArr
because they are only used once then ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah it could, it's just less expensive since we already have the booleans, and isPlainObject
does a lot
@coderabbitai
coderabbitai
bot
left a comment
There was a problem hiding this 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
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/query-core/src/utils.ts (2)
345-346
: Don’t call a potentially shadowed hasOwnProperty on prototypes
prot.hasOwnProperty('isPrototypeOf')
can throw ifhasOwnProperty
is overridden on that prototype. Use the prototype-safe helper.Apply:
- if (!prot.hasOwnProperty('isPrototypeOf')) { + if (!hasOwn.call(prot, 'isPrototypeOf')) {
327-356
: Allow null-prototype objects in isPlainObject
Relax the final prototype check so thatObject.getPrototypeOf(o) === null
is treated as plain (e.g. changeif (Object.getPrototypeOf(o) !== Object.prototype) { return false }to something like
const proto = Object.getPrototypeOf(o) if (proto !== Object.prototype && proto !== null) { return false }) to restore the existing test behavior in utils.test.tsx.
♻️ Duplicate comments (1)
packages/query-core/src/utils.ts (1)
248-248
: Prototype-safe own-property helper: LGTMUsing a cached reference and calling via
.call
avoids shadowing risks. This addresses the earlier review concern.
🧹 Nitpick comments (1)
packages/query-core/src/utils.ts (1)
322-324
: Clarify dense-array intent in isPlainArrayMinor: add a short doc comment noting that sparse arrays or arrays with extra enumerable props return false.
+/** True only for dense Arrays with no extra enumerable properties (sparse arrays excluded). */ export function isPlainArray(value: unknown): value is Array<unknown> { return Array.isArray(value) && value.length === Object.keys(value).length }
📜 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.
📒 Files selected for processing (1)
packages/query-core/src/utils.ts
(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/query-core/src/utils.ts (1)
packages/query-core/src/index.ts (1)
replaceEqualDeep
(19-19)
⏰ 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/query-core/src/utils.ts (1)
263-300
: replaceEqualDeep hot path and allocation strategy: LGTMEarly bailout + preallocated arrays + cached aItem/bItem lookups are clean and align with the perf goals. The equality accounting also correctly preserves
a
only when sizes match and all items are equal.
Uh oh!
There was an error while loading. Please reload this page.
This PR replicates the changes that were done in TanStack/router#5046.
TL;DR: Improve
replaceEqualDeep
performance by ~1.7xHow?
new Array(bSize)
), to avoid the memory management that happens under the hood every time we add an item to ita[key]
) and instead store it asconst aItem
hasOwnProperty
instead of creating aSet
of the keys, because in theoryhasOwnProperty
is already O(1) for lookups, so we don't need to create an extra data structure for thisBenchmark
Results
EDIT
latest commit has improved perf, but also 2x the amount of code
Summary by CodeRabbit
Bug Fixes
Refactor