perf(router-core): stop structurally sharing search and state in buildLocation - #8382
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthrough
ChangesLocation building and commit behavior
Priority: ⬇️ Low Estimated code review effort: 2 (Simple) | ~15 minutes Change: Bug fix Sequence Diagram(s)sequenceDiagram
participant Caller
participant RouterCore.buildLocation
participant parseLocation
participant History
Caller->>RouterCore.buildLocation: provide search and state
RouterCore.buildLocation-->>Caller: return caller-owned values
Caller->>parseLocation: commit navigation
parseLocation->>History: serialize search and update history
parseLocation-->>Caller: return stabilized location
Suggested reviewers: Merge Risk: ⚪ Minimal · up to The updated location-building behavior is covered across build, commit, and history paths; no actionable merge risk remains. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 2 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🚀 Changeset Version Preview7 package(s) bumped directly, 22 bumped as dependents. 🟩 Patch bumps
|
|
View your CI Pipeline Execution ↗ for commit e14d907
☁️ Nx Cloud last updated this comment at |
Bundle Size Benchmarks
The following scenarios have bundle-size changes compared with the baseline:
Current gzip tracks all emitted client JS chunks. Initial gzip tracks only the entry/import graph. Trend sparkline is historical current gzip ending with this PR measurement; lower is better. |
Merging this PR will improve performance by 3.88%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ⚡ | Simulation | client-links navigation loop (vue) |
321.4 ms | 304.9 ms | +5.39% |
| ⚡ | Simulation | client-search-params navigation loop (vue) |
152.2 ms | 147.5 ms | +3.17% |
| ⚡ | Simulation | client-links navigation loop (solid) |
497.4 ms | 482.4 ms | +3.11% |
Tip
Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.
Comparing schiller-manuel-remove-build-location-sharing (e14d907) with schiller-manuel-link-nested-value-stability (98a9b40)
…dLocation
`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>
f5c4a4d to
e14d907
Compare
Stacked on #8380.
🎯 Changes
buildLocationrannullReplaceEqualDeepover the builtsearchandreplaceEqualDeepover the builtstateagainst the current location on every build. That work only affected object identity, and nothing reads that identity from a built location:hrefandisActivefrom the built location; active state is computed by value and ignores history state.commitLocationalready compares state by value (deepEqualover_getUserHistoryState), so pre-shared children were at most a shortcut there.location.state.useror a nested search value without rerendering) comes fromparseLocation, which stabilizes the committed location against the previous one. That sharing is untouched, as are thematchRoutessites for search, params and loaderDeps. Both helpers stay imported and used.On the server
replaceEqualDeepwas already a passthrough, so client and server builds now produce the same shapes.Removed lines (
packages/router-core/src/router.ts, insidebuildLocation'sbuild)nextSearchbecame aconst;nextStateis now a singleconstconditional (EMPTY_RECORDwithoutdest.state,current().statefortrue,dest.state(current().state)for a function, otherwisedest.state). TheusedCurrenttracking is unchanged: only reads throughcurrent()/currentMatch()mark a build as location-dependent, so literal search/state keep hitting the per-options cache from #8370.lightweight[2]is still returned forfromSearch().Behavior changes
searchorstateis returned as the caller's object;buildLocation(...).search.nestedis no longer reference-equal to the current location's nested value. After the navigation commits,router.state.location.search.nested/.state.nesteddo keep the previous reference (viaparseLocation).replaceEqualDeepreturned the current search when contents were equal,buildLocationused 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=1and pushes a new history entry.stateobject flows straight tocommitLocation; that path already copies before adding__hashScrollIntoViewOptions, and@tanstack/historycopies before adding__TSR_key/key/index. Pinned by tests (plain and frozen state objects).searchyields the frozenEMPTY_RECORDrather than a fresh or shared object. Nothing downstream mutates the built search (all consumers copy); the full unit suites pass.Tests
explicit state structurally shares unchanged nested values(asserted build-time sharing).describe('buildLocation - no structural sharing with the current location'):stateis the caller's object; afternavigate,location.state.userkeeps the previous referencesearchwith a nestedfiltersubtreenavigatedoes not mutate a caller-suppliedstate(keys unchanged; a frozen state commits without throwing)buildLocationlevel (searchStr/hreffollow the requested order)history.lengthunchanged; different order → new entry with?b=2&a=1)utils.test.tsline 951 testsreplaceEqualDeepitself and is untouched).Measurements
pnpm benchmark:bundle-size:run --scenario react-router.minimal: gzip 86026 → 86004 (−22), initial −23, raw −41, brotli +65.Verification
@tanstack/router-core:test:unit133 files / 3303 passed@tanstack/react-router:test:unit91 files / 1174 passed@tanstack/solid-router:test:unit68 + 7 files / 942 + 51 passed@tanstack/vue-router:test:unit71 files / 968 passed@tanstack/router-core:test:types,@tanstack/router-core:test:eslint(0 errors),git diff --check, prettier✅ Checklist
🚀 Release Impact
Summary by CodeRabbit
Bug Fixes
buildLocationno longer sharessearchandstateobjects with the current location before navigation is committed.Tests