From e6029dba5b32cda46009db493225f193fa5e296f Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sun, 31 Aug 2025 09:44:42 +0200 Subject: [PATCH 1/6] refactor(query-core): improve replaceEqualDeep performance --- packages/query-core/src/utils.ts | 55 ++++++++++++++++---------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts index f4bde86c8d..ef81f3b5c2 100644 --- a/packages/query-core/src/utils.ts +++ b/packages/query-core/src/utils.ts @@ -257,38 +257,39 @@ export function replaceEqualDeep(a: any, b: any): any { } const array = isPlainArray(a) && isPlainArray(b) - - if (array || (isPlainObject(a) && isPlainObject(b))) { - const aItems = array ? a : Object.keys(a) - const aSize = aItems.length - const bItems = array ? b : Object.keys(b) - const bSize = bItems.length - const copy: any = array ? [] : {} - const aItemsSet = new Set(aItems) - - let equalItems = 0 - - for (let i = 0; i < bSize; i++) { - const key = array ? i : bItems[i] - if ( - ((!array && aItemsSet.has(key)) || array) && - a[key] === undefined && - b[key] === undefined - ) { - copy[key] = undefined + const object = !array && isPlainObject(a) && isPlainObject(b) + + if (!array && !object) return b + + const aItems = array ? a : Object.keys(a) + const aSize = aItems.length + const bItems = array ? b : Object.keys(b) + const bSize = bItems.length + const copy: any = array ? new Array(bSize) : {} + // const aItemsSet = new Set(aItems) + + let equalItems = 0 + + for (let i = 0; i < bSize; i++) { + const key = array ? i : bItems[i] + const aItem = a[key] + if ( + (array || a.hasOwnProperty(key)) && + aItem === undefined && + b[key] === undefined + ) { + copy[key] = undefined + equalItems++ + } else { + const value = replaceEqualDeep(aItem, b[key]) + copy[key] = value + if (value === aItem && aItem !== undefined) { equalItems++ - } else { - copy[key] = replaceEqualDeep(a[key], b[key]) - if (copy[key] === a[key] && a[key] !== undefined) { - equalItems++ - } } } - - return aSize === bSize && equalItems === aSize ? a : copy } - return b + return aSize === bSize && equalItems === aSize ? a : copy } /** From 5e527f39b0ade2cfcefcfd80e3cfa212308c5cf0 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sun, 31 Aug 2025 09:54:43 +0200 Subject: [PATCH 2/6] try object.prototype that broke eslint in router --- packages/query-core/src/utils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts index ef81f3b5c2..54359a8db1 100644 --- a/packages/query-core/src/utils.ts +++ b/packages/query-core/src/utils.ts @@ -266,7 +266,6 @@ export function replaceEqualDeep(a: any, b: any): any { const bItems = array ? b : Object.keys(b) const bSize = bItems.length const copy: any = array ? new Array(bSize) : {} - // const aItemsSet = new Set(aItems) let equalItems = 0 @@ -274,7 +273,7 @@ export function replaceEqualDeep(a: any, b: any): any { const key = array ? i : bItems[i] const aItem = a[key] if ( - (array || a.hasOwnProperty(key)) && + (array || Object.prototype.hasOwnProperty.call(a, key)) && aItem === undefined && b[key] === undefined ) { From 1007495822310a56cd76f5b4c9a5756d55ecf806 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sun, 31 Aug 2025 12:11:53 +0200 Subject: [PATCH 3/6] no object flag --- packages/query-core/src/utils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts index 54359a8db1..349958fdf9 100644 --- a/packages/query-core/src/utils.ts +++ b/packages/query-core/src/utils.ts @@ -257,9 +257,8 @@ export function replaceEqualDeep(a: any, b: any): any { } const array = isPlainArray(a) && isPlainArray(b) - const object = !array && isPlainObject(a) && isPlainObject(b) - if (!array && !object) return b + if (!array && !(isPlainObject(a) && isPlainObject(b))) return b const aItems = array ? a : Object.keys(a) const aSize = aItems.length From 25b5bbd782844eb1369a29cddf231b0ce28f1f87 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sun, 31 Aug 2025 19:02:23 +0200 Subject: [PATCH 4/6] improved version w/ more code --- packages/query-core/src/utils.ts | 98 +++++++++++++++++++++++--------- 1 file changed, 71 insertions(+), 27 deletions(-) diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts index 349958fdf9..0978a8d684 100644 --- a/packages/query-core/src/utils.ts +++ b/packages/query-core/src/utils.ts @@ -245,46 +245,91 @@ export function partialMatchKey(a: any, b: any): boolean { return false } +const hasOwn = Object.prototype.hasOwnProperty + /** * This function returns `a` if `b` is deeply equal. * If not, it will replace any deeply equal children of `b` with those of `a`. * This can be used for structural sharing between JSON values for example. */ export function replaceEqualDeep(a: unknown, b: T): T -export function replaceEqualDeep(a: any, b: any): any { +export function replaceEqualDeep(a: unknown, b: unknown): any { if (a === b) { return a } - const array = isPlainArray(a) && isPlainArray(b) + const aIsArr = isPlainArray(a) + const bIsArr = isPlainArray(b) + + // both are arrays + if (aIsArr && bIsArr) { + const aSize = a.length + const bSize = b.length + const copy: Array = new Array(bSize) + let equalItems = 0 + + for (let i = 0; i < bSize; i++) { + const aItem = a[i] + const bItem = b[i] + + // most common case (strict equality) + if (aItem === bItem) { + copy[i] = aItem + if (i < aSize) equalItems++ + continue + } + + // either item is not an array or object + if (aItem === null || bItem === null || typeof aItem !== 'object' || typeof bItem !== 'object') { + copy[i] = bItem + continue + } + + const v = replaceEqualDeep(aItem, bItem) + copy[i] = v + if (v === aItem) equalItems++ + } + + return aSize === bSize && equalItems === aSize ? a : copy + } - if (!array && !(isPlainObject(a) && isPlainObject(b))) return b + // only 1 is an array + if (aIsArr || bIsArr) { + return b + } - const aItems = array ? a : Object.keys(a) - const aSize = aItems.length - const bItems = array ? b : Object.keys(b) - const bSize = bItems.length - const copy: any = array ? new Array(bSize) : {} + // at least 1 is not an object + if (!isPlainObject(a) || !isPlainObject(b)) { + return b + } + const aSize = Object.keys(a).length + const copy: Record = {} let equalItems = 0 + let bSize = 0 - for (let i = 0; i < bSize; i++) { - const key = array ? i : bItems[i] - const aItem = a[key] - if ( - (array || Object.prototype.hasOwnProperty.call(a, key)) && - aItem === undefined && - b[key] === undefined - ) { - copy[key] = undefined - equalItems++ - } else { - const value = replaceEqualDeep(aItem, b[key]) - copy[key] = value - if (value === aItem && aItem !== undefined) { - equalItems++ - } + for (const k in b) { + bSize++ + + const aItem = a[k] + const bItem = b[k] + + // most common case (strict equality) + if (aItem === bItem) { + copy[k] = aItem + if (hasOwn.call(a, k)) equalItems++ + continue + } + + // either item is not an array or object + if (aItem === null || bItem === null || typeof aItem !== 'object' || typeof bItem !== 'object') { + copy[k] = bItem + continue } + + const v = replaceEqualDeep(aItem, bItem) + copy[k] = v + if (v === aItem) equalItems++ } return aSize === bSize && equalItems === aSize ? a : copy @@ -310,13 +355,12 @@ export function shallowEqualObjects>( return true } -export function isPlainArray(value: unknown) { +export function isPlainArray(value: unknown): value is Array { return Array.isArray(value) && value.length === Object.keys(value).length } // Copied from: https://github.com/jonschlinkert/is-plain-object -// eslint-disable-next-line @typescript-eslint/no-wrapper-object-types -export function isPlainObject(o: any): o is Object { +export function isPlainObject(o: any): o is Record { if (!hasObjectPrototype(o)) { return false } From 3c69c311ce719dcf12157533418b57c74ca70c39 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 31 Aug 2025 17:03:14 +0000 Subject: [PATCH 5/6] ci: apply automated fixes --- packages/query-core/src/utils.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts index 0978a8d684..b0167c6f28 100644 --- a/packages/query-core/src/utils.ts +++ b/packages/query-core/src/utils.ts @@ -280,9 +280,14 @@ export function replaceEqualDeep(a: unknown, b: unknown): any { } // either item is not an array or object - if (aItem === null || bItem === null || typeof aItem !== 'object' || typeof bItem !== 'object') { - copy[i] = bItem - continue + if ( + aItem === null || + bItem === null || + typeof aItem !== 'object' || + typeof bItem !== 'object' + ) { + copy[i] = bItem + continue } const v = replaceEqualDeep(aItem, bItem) @@ -322,7 +327,12 @@ export function replaceEqualDeep(a: unknown, b: unknown): any { } // either item is not an array or object - if (aItem === null || bItem === null || typeof aItem !== 'object' || typeof bItem !== 'object') { + if ( + aItem === null || + bItem === null || + typeof aItem !== 'object' || + typeof bItem !== 'object' + ) { copy[k] = bItem continue } From 91494ca971292740b86771cac7629d8730ed1c73 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Mon, 1 Sep 2025 15:55:12 +0200 Subject: [PATCH 6/6] less code, but still 2x faster --- packages/query-core/src/utils.ts | 78 +++++++------------------------- 1 file changed, 16 insertions(+), 62 deletions(-) diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts index b0167c6f28..fc3a47a210 100644 --- a/packages/query-core/src/utils.ts +++ b/packages/query-core/src/utils.ts @@ -253,92 +253,46 @@ const hasOwn = Object.prototype.hasOwnProperty * This can be used for structural sharing between JSON values for example. */ export function replaceEqualDeep(a: unknown, b: T): T -export function replaceEqualDeep(a: unknown, b: unknown): any { +export function replaceEqualDeep(a: any, b: any): any { if (a === b) { return a } - const aIsArr = isPlainArray(a) - const bIsArr = isPlainArray(b) - - // both are arrays - if (aIsArr && bIsArr) { - const aSize = a.length - const bSize = b.length - const copy: Array = new Array(bSize) - let equalItems = 0 - - for (let i = 0; i < bSize; i++) { - const aItem = a[i] - const bItem = b[i] - - // most common case (strict equality) - if (aItem === bItem) { - copy[i] = aItem - if (i < aSize) equalItems++ - continue - } - - // either item is not an array or object - if ( - aItem === null || - bItem === null || - typeof aItem !== 'object' || - typeof bItem !== 'object' - ) { - copy[i] = bItem - continue - } - - const v = replaceEqualDeep(aItem, bItem) - copy[i] = v - if (v === aItem) equalItems++ - } + const array = isPlainArray(a) && isPlainArray(b) - return aSize === bSize && equalItems === aSize ? a : copy - } + if (!array && !(isPlainObject(a) && isPlainObject(b))) return b - // only 1 is an array - if (aIsArr || bIsArr) { - return b - } + const aItems = array ? a : Object.keys(a) + const aSize = aItems.length + const bItems = array ? b : Object.keys(b) + const bSize = bItems.length + const copy: any = array ? new Array(bSize) : {} - // at least 1 is not an object - if (!isPlainObject(a) || !isPlainObject(b)) { - return b - } - - const aSize = Object.keys(a).length - const copy: Record = {} let equalItems = 0 - let bSize = 0 - - for (const k in b) { - bSize++ - const aItem = a[k] - const bItem = b[k] + for (let i = 0; i < bSize; i++) { + const key: any = array ? i : bItems[i] + const aItem = a[key] + const bItem = b[key] - // most common case (strict equality) if (aItem === bItem) { - copy[k] = aItem - if (hasOwn.call(a, k)) equalItems++ + copy[key] = aItem + if (array ? i < aSize : hasOwn.call(a, key)) equalItems++ continue } - // either item is not an array or object if ( aItem === null || bItem === null || typeof aItem !== 'object' || typeof bItem !== 'object' ) { - copy[k] = bItem + copy[key] = bItem continue } const v = replaceEqualDeep(aItem, bItem) - copy[k] = v + copy[key] = v if (v === aItem) equalItems++ }