df236372 perf(router-core): stop structurally sharing search and state in buildLocation
`buildLocation` ran `nullReplaceEqualDeep` over the built search and
`replaceEqualDeep` over the built state against the current location on
every build. That work only affected object identity, and nothing reads
that identity from a built location:
- Links select href and isActive from the built location; active state is
computed by value and ignores history state.
- `commitLocation` already compares state by value (`deepEqual` over
`_getUserHistoryState`), so pre-shared children were at most a shortcut.
- The identity consumers actually rely on (selecting `location.state.user`
or a nested search value without rerendering) comes from `parseLocation`,
which stabilizes the committed location against the previous one. That
sharing stays untouched, as do the `matchRoutes` sites for search, params
and loaderDeps.
On the server `replaceEqualDeep` was already a passthrough, so client and
server builds now produce the same shapes.
Behavior changes:
- A literal `search` or `state` is returned as the caller's object. It is
never written to: `commitLocation` and history both copy before adding
`__hashScrollIntoViewOptions`, `__TSR_key`, `key` and the index.
- Because `replaceEqualDeep` returned the *current* search when contents
were equal, `buildLocation` used to re-serialize the current key order.
At `?a=1&b=2`, `buildLocation({ search: { b: 2, a: 1 } })` produced
`?a=1&b=2`, so navigating there was a same-location no-op. It now
produces `?b=2&a=1` and pushes a new history entry.
- A destination without `search` yields the frozen `EMPTY_RECORD`
instead of a fresh or shared object. Nothing downstream mutates the
built search; all consumers copy.
The `usedCurrent` tracking is unchanged: only reads through `current()` /
`currentMatch()` mark a build as location-dependent, so literal search and
state keep hitting the per-options cache.
Tests: the build-time sharing assertion (`explicit state structurally
shares unchanged nested values`) is replaced by a describe block that pins
the new contract: built search/state are the caller's objects, equal nested
references are preserved after navigation via parseLocation, `navigate`
does not mutate a plain or frozen caller state, and the key-order case is
covered at both the buildLocation and the navigation level.
Measurements (react-router.minimal, this tree):
gzip 86026 -> 86004 (-22), initial -23, raw -41, brotli +65.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> f5c4a4d7 perf(router-core): stop structurally sharing search and state in buildLocation
`buildLocation` ran `nullReplaceEqualDeep` over the built search and
`replaceEqualDeep` over the built state against the current location on
every build. That work only affected object identity, and nothing reads
that identity from a built location:
- Links select href and isActive from the built location; active state is
computed by value and ignores history state.
- `commitLocation` already compares state by value (`deepEqual` over
`_getUserHistoryState`), so pre-shared children were at most a shortcut.
- The identity consumers actually rely on (selecting `location.state.user`
or a nested search value without rerendering) comes from `parseLocation`,
which stabilizes the committed location against the previous one. That
sharing stays untouched, as do the `matchRoutes` sites for search, params
and loaderDeps.
On the server `replaceEqualDeep` was already a passthrough, so client and
server builds now produce the same shapes.
Behavior changes:
- A literal `search` or `state` is returned as the caller's object. It is
never written to: `commitLocation` and history both copy before adding
`__hashScrollIntoViewOptions`, `__TSR_key`, `key` and the index.
- Because `replaceEqualDeep` returned the *current* search when contents
were equal, `buildLocation` used to re-serialize the current key order.
At `?a=1&b=2`, `buildLocation({ search: { b: 2, a: 1 } })` produced
`?a=1&b=2`, so navigating there was a same-location no-op. It now
produces `?b=2&a=1` and pushes a new history entry.
- A destination without `search` yields the frozen `EMPTY_RECORD`
instead of a fresh or shared object. Nothing downstream mutates the
built search; all consumers copy.
The `usedCurrent` tracking is unchanged: only reads through `current()` /
`currentMatch()` mark a build as location-dependent, so literal search and
state keep hitting the per-options cache.
Tests: the build-time sharing assertion (`explicit state structurally
shares unchanged nested values`) is replaced by a describe block that pins
the new contract: built search/state are the caller's objects, equal nested
references are preserved after navigation via parseLocation, `navigate`
does not mutate a plain or frozen caller state, and the key-order case is
covered at both the buildLocation and the navigation level.
Measurements (react-router.minimal, this tree):
gzip 86026 -> 86004 (-22), initial -23, raw -41, brotli +65.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>