From 1f197adf97223f73ca23f714360b86e1b6703a8e Mon Sep 17 00:00:00 2001 From: Manuel Schiller <6340397+schiller-manuel@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:42:35 +0200 Subject: [PATCH] perf(router-core): match the current location lazily in buildLocation `buildLocation` matched the current location up front for every build, although the result is only read by relative destinations, inherited params, `search: true` or updater functions and search middlewares. An absolute destination with its params given, on a branch without search middlewares, never reads it, yet still paid three store reads and a WeakMap lookup per call. The match is now computed on first read, and the destination template is scanned for `$` once instead of twice. Server `buildLocation` of `/items/$itemId` with params on a middleware-free tree: 399 -> 382 ns per call (-4%). Builds that read the current match are unchanged. react-router.minimal gzip +4 (86008 -> 86012, the stack base). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- packages/router-core/src/router.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index e97d1e9603..fc2b76d979 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -1928,18 +1928,17 @@ export class RouterCore< const currentLocation = dest._fromLocation || this._pendingLocation || this.latestLocation - // Use lightweight matching - only computes what buildLocation needs - // (fullPath, search, params) without creating full match objects - const lightweight = this.matchRoutesLightweight(currentLocation) - // Value-affecting reads of the current location go through these two. + // The lightweight match (fullPath, search, params without full match + // objects) is only computed when a build actually reads it. + let lightweight: LightweightRouteMatchResult | undefined const current = () => { usedCurrent = true return currentLocation } const currentMatch = () => { usedCurrent = true - return lightweight + return (lightweight ??= this.matchRoutesLightweight(currentLocation)) } // check that from path exists in the current route tree @@ -1982,10 +1981,11 @@ export class RouterCore< trimPathRight(nextTo) as keyof typeof this.routesByPath ] as AnyRoute | undefined + const isTemplate = nextTo.includes('$') let destRoutes: ReadonlyArray if (destRoute) { destRoutes = destRoute._branch ??= buildRouteBranch(destRoute) - } else if (nextTo.includes('$')) { + } else if (isTemplate) { // Route templates must match routesByPath exactly. A miss here is a // typed destination mismatch, not a concrete URL to route-match. destRoutes = [] @@ -2003,7 +2003,7 @@ export class RouterCore< } // One parsed template serves both trailing-slash variants. - const interpolation = nextTo.includes('$') + const interpolation = isTemplate ? (destRoute?._interpolation ?? parseSegments(false, { fullPath: nextTo }, 0)) : undefined