Skip to content

Commit eb6bcea

Browse files
refactor(router-core): slightly faster replaceEqualDeep (#5046)
## 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 ```ts 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] }) }) }) ``` ```sh ✓ @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 ```sh ✓ @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 ``` --- <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 48551b3 commit eb6bcea

File tree

1 file changed

+43
-42
lines changed

1 file changed

+43
-42
lines changed

packages/router-core/src/utils.ts

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -217,58 +217,59 @@ export function replaceEqualDeep<T>(prev: any, _next: T): T {
217217
const next = _next as any
218218

219219
const array = isPlainArray(prev) && isPlainArray(next)
220+
const object = !array && isPlainObject(prev) && isPlainObject(next)
220221

221-
if (array || (isSimplePlainObject(prev) && isSimplePlainObject(next))) {
222-
const prevItems = array
223-
? prev
224-
: (Object.keys(prev) as Array<unknown>).concat(
225-
Object.getOwnPropertySymbols(prev),
226-
)
227-
const prevSize = prevItems.length
228-
const nextItems = array
229-
? next
230-
: (Object.keys(next) as Array<unknown>).concat(
231-
Object.getOwnPropertySymbols(next),
232-
)
233-
const nextSize = nextItems.length
234-
const copy: any = array ? [] : {}
235-
236-
let equalItems = 0
237-
238-
for (let i = 0; i < nextSize; i++) {
239-
const key = array ? i : (nextItems[i] as any)
240-
if (
241-
((!array && prevItems.includes(key)) || array) &&
242-
prev[key] === undefined &&
243-
next[key] === undefined
244-
) {
245-
copy[key] = undefined
222+
if (!array && !object) return next
223+
224+
const prevItems = array ? prev : getEnumerableOwnKeys(prev)
225+
if (!prevItems) return next
226+
const nextItems = array ? next : getEnumerableOwnKeys(next)
227+
if (!nextItems) return next
228+
const prevSize = prevItems.length
229+
const nextSize = nextItems.length
230+
const copy: any = array ? new Array(nextSize) : {}
231+
232+
let equalItems = 0
233+
234+
for (let i = 0; i < nextSize; i++) {
235+
const key = array ? i : (nextItems[i] as any)
236+
const p = prev[key]
237+
if (
238+
(array || prev.hasOwnProperty(key)) &&
239+
p === undefined &&
240+
next[key] === undefined
241+
) {
242+
copy[key] = undefined
243+
equalItems++
244+
} else {
245+
const value = replaceEqualDeep(p, next[key])
246+
copy[key] = value
247+
if (value === p && p !== undefined) {
246248
equalItems++
247-
} else {
248-
copy[key] = replaceEqualDeep(prev[key], next[key])
249-
if (copy[key] === prev[key] && prev[key] !== undefined) {
250-
equalItems++
251-
}
252249
}
253250
}
254-
255-
return prevSize === nextSize && equalItems === prevSize ? prev : copy
256251
}
257252

258-
return next
253+
return prevSize === nextSize && equalItems === prevSize ? prev : copy
259254
}
260255

261256
/**
262-
* A wrapper around `isPlainObject` with additional checks to ensure that it is not
263-
* only a plain object, but also one that is "clone-friendly" (doesn't have any
264-
* non-enumerable properties).
257+
* Equivalent to `Reflect.ownKeys`, but ensures that objects are "clone-friendly":
258+
* will return false if object has any non-enumerable properties.
265259
*/
266-
function isSimplePlainObject(o: any) {
267-
return (
268-
// all the checks from isPlainObject are more likely to hit so we perform them first
269-
isPlainObject(o) &&
270-
Object.getOwnPropertyNames(o).length === Object.keys(o).length
271-
)
260+
function getEnumerableOwnKeys(o: object) {
261+
const keys = []
262+
const names = Object.getOwnPropertyNames(o)
263+
for (const name of names) {
264+
if (!Object.prototype.propertyIsEnumerable.call(o, name)) return false
265+
keys.push(name)
266+
}
267+
const symbols = Object.getOwnPropertySymbols(o)
268+
for (const symbol of symbols) {
269+
if (!Object.prototype.propertyIsEnumerable.call(o, symbol)) return false
270+
keys.push(symbol)
271+
}
272+
return keys
272273
}
273274

274275
// Copied from: https://github.com/jonschlinkert/is-plain-object

0 commit comments

Comments
 (0)