From b745f7d39f7cda4abea66ed6958b20615422eb7a Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sun, 22 Mar 2026 18:50:30 +0100 Subject: [PATCH 1/2] perf(router-core): cache current-location lightweight matches --- packages/router-core/src/router.ts | 65 ++++++++++++++++-------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 965fefda339..a485fa5b483 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -767,6 +767,17 @@ export interface MatchRoutesFn { ): Array } +type MatchRoutesLightweightResult = { + matchedRoutes: ReadonlyArray + fullPath: string + params: Record +} + +type MatchRoutesLightweightCache = { + location: ParsedLocation + result: MatchRoutesLightweightResult +} + export type GetMatchFn = (matchId: string) => AnyRouteMatch | undefined export type UpdateMatchFn = ( @@ -1674,44 +1685,28 @@ export class RouterCore< }) } + private matchRoutesLightweightCache?: MatchRoutesLightweightCache /** * Lightweight route matching for buildLocation. * Only computes fullPath, accumulated search, and params - skipping expensive * operations like AbortController, ControlledPromise, loaderDeps, and full match objects. */ - private matchRoutesLightweight(location: ParsedLocation): { - matchedRoutes: ReadonlyArray - fullPath: string - search: Record - params: Record - } { + private matchRoutesLightweight( + location: ParsedLocation, + ): MatchRoutesLightweightResult { + const isCurrent = this.stores.location.state === location + if (isCurrent) { + const cached = this.matchRoutesLightweightCache + if (cached?.location === location) { + return cached.result + } + } + const { matchedRoutes, routeParams, parsedParams } = this.getMatchedRoutes( location.pathname, ) const lastRoute = last(matchedRoutes)! - // I don't know if we should run the full search middleware chain, or just validateSearch - // // Accumulate search validation through the route chain - // const accumulatedSearch: Record = applySearchMiddleware({ - // search: { ...location.search }, - // dest: location, - // destRoutes: matchedRoutes, - // _includeValidateSearch: true, - // }) - - // Accumulate search validation through route chain - const accumulatedSearch = { ...location.search } - for (const route of matchedRoutes) { - try { - Object.assign( - accumulatedSearch, - validateSearch(route.options.validateSearch, accumulatedSearch), - ) - } catch { - // Ignore errors, we're not actually routing - } - } - // Determine params: reuse from state if possible, otherwise parse const lastStateMatchId = last(this.stores.matchesId.state) const lastStateMatch = @@ -1746,12 +1741,20 @@ export class RouterCore< params = strictParams } - return { + const result = { matchedRoutes, fullPath: lastRoute.fullPath, - search: accumulatedSearch, params, } + + if (isCurrent) { + this.matchRoutesLightweightCache = { + location, + result, + } + } + + return result } cancelMatch = (id: string) => { @@ -1839,7 +1842,7 @@ export class RouterCore< const fromPath = this.resolvePathWithBase(defaultedFromPath, '.') // From search should always use the current location - const fromSearch = lightweightResult.search + const fromSearch = currentLocation.search // Same with params. It can't hurt to provide as many as possible const fromParams = Object.assign( Object.create(null), From b845204fa72679bf69e59722da6b4a26be3f44a1 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sun, 22 Mar 2026 19:23:43 +0100 Subject: [PATCH 2/2] fix(router-core): preserve validated search in buildLocation --- packages/router-core/src/router.ts | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index a485fa5b483..17b3a357f06 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -770,6 +770,7 @@ export interface MatchRoutesFn { type MatchRoutesLightweightResult = { matchedRoutes: ReadonlyArray fullPath: string + search: Record params: Record } @@ -984,6 +985,7 @@ export class RouterCore< isServer!: boolean pathParamsDecoder?: (encoded: string) => string protocolAllowlist!: Set + private matchRoutesLightweightCache?: MatchRoutesLightweightCache /** * @deprecated Use the `createRouter` function instead @@ -1057,6 +1059,8 @@ export class RouterCore< ...newOptions, } + this.matchRoutesLightweightCache = undefined + this.isServer = this.options.isServer ?? typeof document === 'undefined' this.protocolAllowlist = new Set(this.options.protocolAllowlist) @@ -1685,11 +1689,10 @@ export class RouterCore< }) } - private matchRoutesLightweightCache?: MatchRoutesLightweightCache /** * Lightweight route matching for buildLocation. - * Only computes fullPath, accumulated search, and params - skipping expensive - * operations like AbortController, ControlledPromise, loaderDeps, and full match objects. + * Returns matched routes, full path, validated search, and params without + * creating full match objects. */ private matchRoutesLightweight( location: ParsedLocation, @@ -1707,6 +1710,18 @@ export class RouterCore< ) const lastRoute = last(matchedRoutes)! + const search = { ...location.search } + for (const route of matchedRoutes) { + try { + Object.assign( + search, + validateSearch(route.options.validateSearch, search), + ) + } catch { + // Ignore errors, we're not actually routing + } + } + // Determine params: reuse from state if possible, otherwise parse const lastStateMatchId = last(this.stores.matchesId.state) const lastStateMatch = @@ -1744,6 +1759,7 @@ export class RouterCore< const result = { matchedRoutes, fullPath: lastRoute.fullPath, + search, params, } @@ -1841,8 +1857,8 @@ export class RouterCore< // ensure this includes the basePath if set const fromPath = this.resolvePathWithBase(defaultedFromPath, '.') - // From search should always use the current location - const fromSearch = currentLocation.search + // From search should use the validated snapshot for the from location + const fromSearch = { ...lightweightResult.search } // Same with params. It can't hurt to provide as many as possible const fromParams = Object.assign( Object.create(null),